package zddc import "testing" func TestGlobMatch(t *testing.T) { cases := []struct { pattern string s string want bool }{ // Literal (no wildcard) {"alice", "alice", true}, {"alice", "bob", false}, {"", "", true}, {"", "x", false}, // Lone wildcard {"*", "anything", true}, {"*", "", true}, // Prefix wildcard {"*@example.com", "@example.com", true}, {"*.com", "example.com", true}, {"*.com", "example.org", false}, // Suffix wildcard {"alice*", "alice", true}, {"alice*", "alice@example.com", true}, {"alice*", "bob", false}, // Middle wildcard {"a*b", "ab", true}, {"a*b", "axxxb", true}, {"a*b", "axxx", false}, {"a*b", "xxxb", false}, // Multiple wildcards {"*-*-*", "a-b-c", true}, {"*-*-*", "ab-c", false}, // Anchored: no implicit leading wildcard {"alice@*", "bob@example.com", false}, } for _, tc := range cases { t.Run(tc.pattern+"|"+tc.s, func(t *testing.T) { if got := globMatch(tc.pattern, tc.s); got != tc.want { t.Errorf("globMatch(%q, %q) = %v, want %v", tc.pattern, tc.s, got, tc.want) } }) } } func TestMatchesPattern(t *testing.T) { cases := []struct { pattern string email string want bool }{ // Exact match {"alice@example.com", "alice@example.com", true}, {"alice@example.com", "bob@example.com", false}, // Wildcard local part, fixed domain {"*@example.com", "alice@example.com", true}, {"*@example.com", "anyone@example.com", true}, {"*@example.com", "alice@evil.com", false}, // Fixed local part, wildcard domain {"alice@*", "alice@example.com", true}, {"alice@*", "alice@evil.com", true}, {"alice@*", "bob@example.com", false}, // @-boundary respected: * in local part does not eat the @ {"alice*", "alice@example.com", true}, // pattern has no @, matches against full email // But splitting on @ for both sides: {"*", "alice@example.com", true}, // lone * matches anything {"*@*", "alice@example.com", true}, {"*@*", "no-at-sign", false}, // pattern has @, email doesn't // Pattern with @, email without {"alice@example.com", "alice", false}, // Empty email: lone "*" should still match per docstring? Actually globMatch("*", "") = true // But MatchesPattern("*", "") splits "*" on @ → ["*"]. Then globMatch("*", "") = true. // The docstring says "matches any non-empty email" but the implementation matches empty too. // Document the actual behavior in the test. {"*", "", true}, } for _, tc := range cases { t.Run(tc.pattern+"|"+tc.email, func(t *testing.T) { if got := MatchesPattern(tc.pattern, tc.email); got != tc.want { t.Errorf("MatchesPattern(%q, %q) = %v, want %v", tc.pattern, tc.email, got, tc.want) } }) } } func TestAllowedAtLevel(t *testing.T) { cases := []struct { name string level ZddcFile email string wantAllowed bool wantMatched bool }{ { name: "no rules: not matched", level: ZddcFile{}, email: "alice@example.com", wantAllowed: false, wantMatched: false, }, { name: "allow matched", level: ZddcFile{ACL: ACLRules{ Allow: []string{"*@example.com"}, }}, email: "alice@example.com", wantAllowed: true, wantMatched: true, }, { name: "deny matched", level: ZddcFile{ACL: ACLRules{ Deny: []string{"alice@example.com"}, }}, email: "alice@example.com", wantAllowed: false, wantMatched: true, }, { name: "deny wins over allow at the same level", level: ZddcFile{ACL: ACLRules{ Allow: []string{"*@example.com"}, Deny: []string{"alice@example.com"}, }}, email: "alice@example.com", wantAllowed: false, wantMatched: true, }, { name: "neither rule matches", level: ZddcFile{ACL: ACLRules{ Allow: []string{"*@example.com"}, Deny: []string{"*@evil.com"}, }}, email: "carol@other.org", wantAllowed: false, wantMatched: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { gotAllowed, gotMatched := AllowedAtLevel(tc.level, tc.email) if gotAllowed != tc.wantAllowed || gotMatched != tc.wantMatched { t.Errorf("AllowedAtLevel(%v, %q) = (%v, %v), want (%v, %v)", tc.level, tc.email, gotAllowed, gotMatched, tc.wantAllowed, tc.wantMatched) } }) } } func TestAllowedWithChain(t *testing.T) { allow := func(p ...string) ZddcFile { return ZddcFile{ACL: ACLRules{Allow: p}} } deny := func(p ...string) ZddcFile { return ZddcFile{ACL: ACLRules{Deny: p}} } allowDeny := func(a, d []string) ZddcFile { return ZddcFile{ACL: ACLRules{Allow: a, Deny: d}} } empty := ZddcFile{} cases := []struct { name string chain PolicyChain email string want bool }{ { name: "empty chain, no files: default allow", chain: PolicyChain{HasAnyFile: false}, email: "alice@example.com", want: true, }, { name: "files exist but no rule matches: default deny", chain: PolicyChain{Levels: []ZddcFile{allow("*@trusted.com")}, HasAnyFile: true}, email: "alice@example.com", want: false, }, { name: "leaf allow wins", chain: PolicyChain{Levels: []ZddcFile{empty, allow("*@example.com")}, HasAnyFile: true}, email: "alice@example.com", want: true, }, { name: "leaf deny beats parent allow (bottom-up first match)", chain: PolicyChain{Levels: []ZddcFile{ allow("*@example.com"), deny("alice@example.com"), }, HasAnyFile: true}, email: "alice@example.com", want: false, }, { name: "leaf has no rule for user, falls back to parent allow", chain: PolicyChain{Levels: []ZddcFile{ allow("*@example.com"), allow("bob@example.com"), // doesn't match alice }, HasAnyFile: true}, email: "alice@example.com", want: true, }, { name: "leaf allows user that parent denies", chain: PolicyChain{Levels: []ZddcFile{ deny("alice@example.com"), allow("alice@example.com"), }, HasAnyFile: true}, email: "alice@example.com", want: true, // leaf wins }, { name: "multi-level: deepest match wins", chain: PolicyChain{Levels: []ZddcFile{ allow("*@example.com"), allowDeny([]string{"*@example.com"}, []string{"alice@example.com"}), allow("alice@example.com"), // deepest re-allows alice }, HasAnyFile: true}, email: "alice@example.com", want: true, }, { name: "no match anywhere with files present: deny", chain: PolicyChain{Levels: []ZddcFile{empty, empty, empty}, HasAnyFile: true}, email: "alice@example.com", want: false, }, { name: "no match anywhere without files: allow", chain: PolicyChain{Levels: []ZddcFile{empty, empty, empty}, HasAnyFile: false}, email: "alice@example.com", want: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := AllowedWithChain(tc.chain, tc.email); got != tc.want { t.Errorf("AllowedWithChain = %v, want %v", got, tc.want) } }) } }