Files
archivmail/internal/api/user_address_api_test.go
patrickandClaude Sonnet 5.5 5248a177ce feat(PROJ-87,PROJ-90): Zusatzadressen pro User und Sichtbarkeit aller Mails eigener IMAP-Konten
Rolle user sieht zusätzlich Mails mit Zusatzadressen (PROJ-87) und alle Mails
von IMAP-Konten, deren owner dem User im selben Tenant entspricht (PROJ-90).
Liste, Nachprüfung und Detailzugriff nutzen dieselbe Regel (mailAccess).
Index: Token imapacct<id> in rcpt_addr, Reindex für Bestandsmails nötig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 12:10:57 +01:00

159 lines
5.8 KiB
Go

package api_test
import (
"encoding/json"
"fmt"
"testing"
"archivmail/internal/userstore"
)
func seedAddressUsers(t *testing.T, env *testEnv) map[string]*userstore.User {
t.Helper()
t1, t2 := int64(1), int64(2)
specs := []struct {
name, role string
tenant *int64
}{
{"root", userstore.RoleSuperAdmin, nil},
{"dom1", userstore.RoleDomainAdmin, &t1},
{"dom2", userstore.RoleDomainAdmin, &t2},
{"aud1", userstore.RoleDomainAuditor, &t1},
{"u1", userstore.RoleUser, &t1},
{"u1b", userstore.RoleUser, &t1},
{"u2", userstore.RoleUser, &t2},
}
out := map[string]*userstore.User{}
for _, sp := range specs {
u, err := env.users.Create(userstore.CreateUserRequest{
Username: sp.name, Email: sp.name + "@example.com", Password: "pw-" + sp.name, Role: sp.role, TenantID: sp.tenant,
})
if err != nil {
t.Fatalf("create %s: %v", sp.name, err)
}
out[sp.name] = u
}
return out
}
func addrPath(id int64) string { return fmt.Sprintf("/api/admin/users/%d/addresses", id) }
func TestUserAddressPermissions(t *testing.T) {
env := newTestEnv(t)
u := seedAddressUsers(t, env)
tok := func(name string) string { return env.login(t, name+"@example.com", "pw-"+name) }
body := map[string]string{"address": "alias1@example.com"}
for _, name := range []string{"u1", "aud1"} {
if w := env.do(t, "POST", addrPath(u["u1"].ID), body, tok(name)); w.Code != 403 {
t.Errorf("%s add: expected 403, got %d", name, w.Code)
}
if w := env.do(t, "GET", addrPath(u["u1"].ID), nil, tok(name)); w.Code != 403 {
t.Errorf("%s list: expected 403, got %d", name, w.Code)
}
}
if w := env.do(t, "GET", addrPath(u["u1"].ID), nil, env.login(t, "auditor", "auditorpass")); w.Code != 403 {
t.Errorf("auditor list: expected 403, got %d", w.Code)
}
dom2 := tok("dom2")
for _, m := range []string{"GET", "POST", "DELETE"} {
w := env.do(t, m, addrPath(u["u1"].ID), body, dom2)
if w.Code != 404 {
t.Errorf("cross-tenant %s: expected 404, got %d", m, w.Code)
}
}
if w := env.do(t, "GET", addrPath(999999), nil, dom2); w.Code != 404 {
t.Errorf("unknown user: expected 404, got %d", w.Code)
}
dom1 := tok("dom1")
if w := env.do(t, "POST", addrPath(u["u1"].ID), body, dom1); w.Code != 201 {
t.Fatalf("dom1 add: expected 201, got %d %s", w.Code, w.Body.String())
}
if w := env.do(t, "POST", addrPath(u["aud1"].ID), map[string]string{"address": "x1@example.com"}, dom1); w.Code != 400 {
t.Errorf("add to non-user role: expected 400, got %d", w.Code)
}
root := tok("root")
if w := env.do(t, "POST", addrPath(u["u2"].ID), map[string]string{"address": "alias2@example.com"}, root); w.Code != 201 {
t.Errorf("superadmin add other tenant: expected 201, got %d %s", w.Code, w.Body.String())
}
w := env.do(t, "GET", addrPath(u["u1"].ID), nil, dom1)
if w.Code != 200 {
t.Fatalf("list: %d", w.Code)
}
var resp struct {
UserID int64 `json:"user_id"`
Max int `json:"max"`
Addresses []struct {
Address string `json:"address"`
CreatedBy string `json:"created_by"`
} `json:"addresses"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp.UserID != u["u1"].ID || resp.Max != userstore.MaxExtraAddresses || len(resp.Addresses) != 1 ||
resp.Addresses[0].Address != "alias1@example.com" || resp.Addresses[0].CreatedBy != "dom1" {
t.Errorf("unexpected list response: %s", w.Body.String())
}
if w := env.do(t, "DELETE", addrPath(u["u1"].ID)+"?address=ALIAS1@example.com", nil, dom1); w.Code != 200 {
t.Errorf("delete: expected 200, got %d", w.Code)
}
if w := env.do(t, "DELETE", addrPath(u["u1"].ID), body, dom1); w.Code != 404 {
t.Errorf("delete again: expected 404, got %d", w.Code)
}
}
func TestUserAddressValidationAndCollision(t *testing.T) {
env := newTestEnv(t)
u := seedAddressUsers(t, env)
dom1 := env.login(t, "dom1@example.com", "pw-dom1")
dom2 := env.login(t, "dom2@example.com", "pw-dom2")
for _, bad := range []string{"", "example.com", "@example.com", "*@example.com", "a?@example.com", "A <a@example.com>"} {
if w := env.do(t, "POST", addrPath(u["u1"].ID), map[string]string{"address": bad}, dom1); w.Code != 400 {
t.Errorf("%q: expected 400, got %d", bad, w.Code)
}
}
if w := env.do(t, "POST", addrPath(u["u1"].ID), map[string]string{"address": " Team@Example.com "}, dom1); w.Code != 201 {
t.Fatalf("add team: %d %s", w.Code, w.Body.String())
}
if w := env.do(t, "POST", addrPath(u["u1"].ID), map[string]string{"address": "team@example.com"}, dom1); w.Code != 409 {
t.Errorf("duplicate: expected 409, got %d", w.Code)
}
if w := env.do(t, "POST", addrPath(u["u1b"].ID), map[string]string{"address": "team@example.com"}, dom1); w.Code != 201 {
t.Errorf("same tenant shared address: expected 201, got %d", w.Code)
}
if w := env.do(t, "POST", addrPath(u["u1b"].ID), map[string]string{"address": "u1@example.com"}, dom1); w.Code != 201 {
t.Errorf("same tenant login address: expected 201, got %d", w.Code)
}
w := env.do(t, "POST", addrPath(u["u2"].ID), map[string]string{"address": "team@example.com"}, dom2)
if w.Code != 409 {
t.Errorf("other tenant extra address: expected 409, got %d", w.Code)
}
var e map[string]string
_ = json.Unmarshal(w.Body.Bytes(), &e)
if e["code"] != "address_unavailable" {
t.Errorf("conflict must use neutral code, got %v", e)
}
if w := env.do(t, "POST", addrPath(u["u2"].ID), map[string]string{"address": "u1@example.com"}, dom2); w.Code != 409 {
t.Errorf("other tenant login address: expected 409, got %d", w.Code)
}
for i := 0; i < userstore.MaxExtraAddresses-1; i++ {
addr := fmt.Sprintf("bulk%d@example.com", i)
if w := env.do(t, "POST", addrPath(u["u1"].ID), map[string]string{"address": addr}, dom1); w.Code != 201 {
t.Fatalf("bulk %d: %d", i, w.Code)
}
}
if w := env.do(t, "POST", addrPath(u["u1"].ID), map[string]string{"address": "one-more@example.com"}, dom1); w.Code != 400 {
t.Errorf("limit: expected 400, got %d", w.Code)
}
}