Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c26c7d039e |
@@ -0,0 +1,458 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// envRequirementKey tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestEnvRequirementKey_SingleMember_Pure(t *testing.T) {
|
||||
key := envRequirementKey([]string{"API_KEY"})
|
||||
if key != "API_KEY" {
|
||||
t.Errorf("single member: got %q, want %q", key, "API_KEY")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvRequirementKey_TwoMembersSorted_Pure(t *testing.T) {
|
||||
// envRequirementKey sorts before joining, so [B, A] and [A, B] produce same key.
|
||||
keyBA := envRequirementKey([]string{"B", "A"})
|
||||
keyAB := envRequirementKey([]string{"A", "B"})
|
||||
if keyBA != keyAB {
|
||||
t.Errorf("sort invariance: got %q vs %q", keyBA, keyAB)
|
||||
}
|
||||
if keyBA != "A\x00B" {
|
||||
t.Errorf("sort result: got %q, want %q", keyBA, "A\x00B")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvRequirementKey_ThreeMembers_Pure(t *testing.T) {
|
||||
key := envRequirementKey([]string{"C", "A", "B"})
|
||||
if key != "A\x00B\x00C" {
|
||||
t.Errorf("three members sorted: got %q, want %q", key, "A\x00B\x00C")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvRequirementKey_Empty_Pure(t *testing.T) {
|
||||
key := envRequirementKey([]string{})
|
||||
if key != "" {
|
||||
t.Errorf("empty: got %q, want %q", key, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvRequirementKey_DedupBySort_Pure(t *testing.T) {
|
||||
// Two lists that are permutations of each other must have identical keys.
|
||||
key1 := envRequirementKey([]string{"Z", "M", "A"})
|
||||
key2 := envRequirementKey([]string{"A", "Z", "M"})
|
||||
if key1 != key2 {
|
||||
t.Errorf("permutation invariance: got %q vs %q", key1, key2)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// sanitizeEnvMembers tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestSanitizeEnvMembers_AllValid_Pure(t *testing.T) {
|
||||
members := []string{"API_KEY", "ANTHROPIC_API_KEY", "SECRET_123"}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if !ok {
|
||||
t.Error("expected ok=true for all valid names")
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Errorf("length: got %d, want 3", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEnvMembers_FiltersLowercase_Pure(t *testing.T) {
|
||||
members := []string{"api_key", "ANTHROPIC_API_KEY", "VALID_NAME"}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if !ok {
|
||||
t.Error("expected ok=true for mixed case")
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Errorf("length: got %d, want 2", len(got))
|
||||
}
|
||||
want := []string{"ANTHROPIC_API_KEY", "VALID_NAME"}
|
||||
for i, w := range want {
|
||||
if got[i] != w {
|
||||
t.Errorf("got[%d]=%q, want %q", i, got[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEnvMembers_FiltersInvalidCharacters_Pure(t *testing.T) {
|
||||
members := []string{"VALID", "in valid", "ALSO-INVALID", "SPACE KEY", "hyphen-key", ""}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if !ok {
|
||||
t.Error("expected ok=true (VALID was present)")
|
||||
}
|
||||
if len(got) != 1 || got[0] != "VALID" {
|
||||
t.Errorf("got %v, want [VALID]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEnvMembers_AllInvalid_Pure(t *testing.T) {
|
||||
members := []string{"lowercase", "has-dash", "has space"}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if ok {
|
||||
t.Error("expected ok=false when all members invalid")
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("got %v, want []", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEnvMembers_EmptyInput_Pure(t *testing.T) {
|
||||
members := []string{}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if ok {
|
||||
t.Error("expected ok=false for empty input")
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("got %v, want []", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEnvMembers_EmptyStringNotLogged_Pure(t *testing.T) {
|
||||
// Empty string is filtered silently (not logged) — verify no panic.
|
||||
members := []string{"VALID", ""}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if !ok || len(got) != 1 || got[0] != "VALID" {
|
||||
t.Errorf("unexpected result: ok=%v got=%v", ok, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEnvMembers_StartsWithDigit_Pure(t *testing.T) {
|
||||
members := []string{"123KEY", "VALID_KEY"}
|
||||
got, ok := sanitizeEnvMembers(members, "test")
|
||||
if !ok {
|
||||
t.Error("expected ok=true (VALID_KEY was present)")
|
||||
}
|
||||
if len(got) != 1 || got[0] != "VALID_KEY" {
|
||||
t.Errorf("got %v, want [VALID_KEY]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// flattenAndSortRequirements tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestFlattenAndSortRequirements_SinglesFirst_Pure(t *testing.T) {
|
||||
by := map[string]EnvRequirement{
|
||||
"B": {Name: "B"},
|
||||
"Z": {Name: "Z"},
|
||||
"group1": {AnyOf: []string{"A", "C"}},
|
||||
}
|
||||
got := flattenAndSortRequirements(by)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("len: got %d, want 3", len(got))
|
||||
}
|
||||
// Singles first, then groups.
|
||||
if got[0].Name == "" {
|
||||
t.Error("first item should be a single")
|
||||
}
|
||||
if got[1].Name == "" {
|
||||
t.Error("second item should be a single")
|
||||
}
|
||||
// Within singles, alphabetical.
|
||||
if got[0].Name != "B" || got[1].Name != "Z" {
|
||||
t.Errorf("got singles %q, %q, want B, Z", got[0].Name, got[1].Name)
|
||||
}
|
||||
// Then groups.
|
||||
if len(got[2].AnyOf) == 0 {
|
||||
t.Error("third item should be a group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAndSortRequirements_GroupsSortedByKey_Pure(t *testing.T) {
|
||||
by := map[string]EnvRequirement{
|
||||
"B\x00A": {AnyOf: []string{"B", "A"}}, // key already sorted
|
||||
"A\x00C": {AnyOf: []string{"A", "C"}},
|
||||
}
|
||||
got := flattenAndSortRequirements(by)
|
||||
// After singles, groups sorted by their envRequirementKey.
|
||||
// "A\x00B" < "A\x00C", so the B/A group (key "A\x00B") comes first.
|
||||
group0 := got[len(got)-2]
|
||||
group1 := got[len(got)-1]
|
||||
if group0.AnyOf[0] != "B" || group1.AnyOf[0] != "A" {
|
||||
t.Errorf("group order: got %v then %v, want [B,A] then [A,C]", group0.AnyOf, group1.AnyOf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAndSortRequirements_EmptyMap_Pure(t *testing.T) {
|
||||
by := map[string]EnvRequirement{}
|
||||
got := flattenAndSortRequirements(by)
|
||||
if len(got) != 0 {
|
||||
t.Errorf("empty map: got %d items, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAndSortRequirements_OnlyGroups_Pure(t *testing.T) {
|
||||
by := map[string]EnvRequirement{
|
||||
"X\x00Y": {AnyOf: []string{"X", "Y"}},
|
||||
}
|
||||
got := flattenAndSortRequirements(by)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("len: got %d, want 1", len(got))
|
||||
}
|
||||
if len(got[0].AnyOf) == 0 {
|
||||
t.Error("only group expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAndSortRequirements_Deterministic_Pure(t *testing.T) {
|
||||
// Run twice; results must be identical.
|
||||
by := map[string]EnvRequirement{
|
||||
"C": {Name: "C"},
|
||||
"A": {Name: "A"},
|
||||
"B": {Name: "B"},
|
||||
"Z\x00Y": {AnyOf: []string{"Z", "Y"}},
|
||||
}
|
||||
got1 := flattenAndSortRequirements(by)
|
||||
got2 := flattenAndSortRequirements(by)
|
||||
if len(got1) != len(got2) {
|
||||
t.Fatalf("len mismatch: %d vs %d", len(got1), len(got2))
|
||||
}
|
||||
for i := range got1 {
|
||||
if got1[i].Name != got2[i].Name || len(got1[i].AnyOf) != len(got2[i].AnyOf) {
|
||||
t.Errorf("item %d differs: %+v vs %+v", i, got1[i], got2[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAndSortRequirements_AllSingles_Pure(t *testing.T) {
|
||||
by := map[string]EnvRequirement{
|
||||
"X": {Name: "X"},
|
||||
"M": {Name: "M"},
|
||||
"A": {Name: "A"},
|
||||
}
|
||||
got := flattenAndSortRequirements(by)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("len: got %d, want 3", len(got))
|
||||
}
|
||||
names := make([]string, len(got))
|
||||
for i, r := range got {
|
||||
names[i] = r.Name
|
||||
}
|
||||
// Must be sorted alphabetically.
|
||||
if !sort.IsSorted(sort.StringSlice(names)) {
|
||||
t.Errorf("not sorted: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// collectOrgEnv tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestCollectOrgEnv_EmptyTemplate_Pure(t *testing.T) {
|
||||
tmpl := &OrgTemplate{}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 0 || len(rec) != 0 {
|
||||
t.Errorf("empty template: got req=%d rec=%d, want 0,0", len(req), len(rec))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_SingleRequired_Pure(t *testing.T) {
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{Name: "API_KEY"}},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 1 {
|
||||
t.Errorf("req count: got %d, want 1", len(req))
|
||||
}
|
||||
if len(rec) != 0 {
|
||||
t.Errorf("rec count: got %d, want 0", len(rec))
|
||||
}
|
||||
if req[0].Name != "API_KEY" {
|
||||
t.Errorf("req[0].Name: got %q, want %q", req[0].Name, "API_KEY")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_SingleRecommended_Pure(t *testing.T) {
|
||||
tmpl := &OrgTemplate{
|
||||
RecommendedEnv: []EnvRequirement{{Name: "DEBUG_MODE"}},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 0 {
|
||||
t.Errorf("req count: got %d, want 0", len(req))
|
||||
}
|
||||
if len(rec) != 1 {
|
||||
t.Errorf("rec count: got %d, want 1", len(rec))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_RequiredWinsOverRecommended_Pure(t *testing.T) {
|
||||
// Same env name in both tiers → appears only in required.
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{Name: "SHARED_KEY"}},
|
||||
RecommendedEnv: []EnvRequirement{{Name: "SHARED_KEY"}},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 1 || len(rec) != 0 {
|
||||
t.Errorf("got req=%d rec=%d, want 1,0", len(req), len(rec))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_StrictDropsAnyOf_Pure(t *testing.T) {
|
||||
// Single required + any-of group containing that name → any-of dropped.
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{Name: "API_KEY"}},
|
||||
RecommendedEnv: []EnvRequirement{{AnyOf: []string{"API_KEY", "FALLBACK_KEY"}}},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 1 || req[0].Name != "API_KEY" {
|
||||
t.Errorf("req: got %+v", req)
|
||||
}
|
||||
if len(rec) != 0 {
|
||||
t.Errorf("rec: got %d, want 0 (any-of dropped by strict required)", len(rec))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_AnyOfGroup_Pure(t *testing.T) {
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{AnyOf: []string{"KEY_A", "KEY_B"}}},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 1 {
|
||||
t.Fatalf("req count: got %d, want 1", len(req))
|
||||
}
|
||||
if len(req[0].AnyOf) != 2 {
|
||||
t.Errorf("req[0].AnyOf: got %v, want [KEY_A, KEY_B]", req[0].AnyOf)
|
||||
}
|
||||
_ = rec // may be unused but call is valid
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_NestedWorkspace_Pure(t *testing.T) {
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{Name: "ORG_KEY"}},
|
||||
Workspaces: []OrgWorkspace{
|
||||
{
|
||||
Name: "child-ws",
|
||||
RequiredEnv: []EnvRequirement{{Name: "CHILD_KEY"}},
|
||||
RecommendedEnv: []EnvRequirement{{Name: "CHILD_RECOM"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
// All 3 required names should appear (dedupped).
|
||||
if len(req) != 2 {
|
||||
t.Errorf("req count: got %d, want 2", len(req))
|
||||
}
|
||||
if len(rec) != 1 {
|
||||
t.Errorf("rec count: got %d, want 1", len(rec))
|
||||
}
|
||||
names := make(map[string]bool)
|
||||
for _, r := range req {
|
||||
names[r.Name] = true
|
||||
}
|
||||
if !names["ORG_KEY"] || !names["CHILD_KEY"] {
|
||||
t.Errorf("req names: got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_InvalidEnvNameFiltered_Pure(t *testing.T) {
|
||||
// Invalid names (lowercase, empty) are silently dropped.
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{
|
||||
{Name: "VALID_KEY"},
|
||||
{Name: "ALSO_VALID"},
|
||||
{Name: "invalid-name"},
|
||||
{Name: ""},
|
||||
},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 2 {
|
||||
t.Errorf("got %d req, want 2 (lowercase and empty filtered)", len(req))
|
||||
}
|
||||
_ = rec
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_DedupSameMembers_Pure(t *testing.T) {
|
||||
// Same members declared twice → deduplicated.
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{
|
||||
{AnyOf: []string{"A", "B"}},
|
||||
{AnyOf: []string{"B", "A"}}, // same set, reversed
|
||||
},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 1 {
|
||||
t.Errorf("dedup failed: got %d req, want 1", len(req))
|
||||
}
|
||||
_ = rec
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_RecDedupedByStrictRequired_Pure(t *testing.T) {
|
||||
// Strict required drops any-of groups in recommended tier too.
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{Name: "STRICT"}},
|
||||
RecommendedEnv: []EnvRequirement{{AnyOf: []string{"STRICT", "OTHER"}}},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(rec) != 0 {
|
||||
t.Errorf("rec should be empty (strict required prunes recommended any-of): got %d", len(rec))
|
||||
}
|
||||
_ = req
|
||||
}
|
||||
|
||||
func TestCollectOrgEnv_RecursiveChildren_Pure(t *testing.T) {
|
||||
tmpl := &OrgTemplate{
|
||||
RequiredEnv: []EnvRequirement{{Name: "ROOT"}},
|
||||
Workspaces: []OrgWorkspace{
|
||||
{
|
||||
Name: "parent",
|
||||
RequiredEnv: []EnvRequirement{{Name: "PARENT"}, {Name: "PARENT2"}},
|
||||
Children: []OrgWorkspace{
|
||||
{
|
||||
Name: "grandchild",
|
||||
RequiredEnv: []EnvRequirement{{Name: "GRANDCHILD"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req, rec := collectOrgEnv(tmpl)
|
||||
if len(req) != 4 {
|
||||
t.Errorf("got %d required, want 4 (ROOT + PARENT + PARENT2 + GRANDCHILD)", len(req))
|
||||
}
|
||||
_ = rec
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// countWorkspaces tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestCountWorkspaces_Empty_Pure(t *testing.T) {
|
||||
if n := countWorkspaces(nil); n != 0 {
|
||||
t.Errorf("nil: got %d, want 0", n)
|
||||
}
|
||||
if n := countWorkspaces([]OrgWorkspace{}); n != 0 {
|
||||
t.Errorf("empty: got %d, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountWorkspaces_Flat_Pure(t *testing.T) {
|
||||
ws := []OrgWorkspace{
|
||||
{Name: "a"}, {Name: "b"},
|
||||
}
|
||||
if n := countWorkspaces(ws); n != 2 {
|
||||
t.Errorf("flat: got %d, want 2", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountWorkspaces_Nested_Pure(t *testing.T) {
|
||||
ws := []OrgWorkspace{
|
||||
{Name: "a", Children: []OrgWorkspace{
|
||||
{Name: "b", Children: []OrgWorkspace{
|
||||
{Name: "c"},
|
||||
}},
|
||||
{Name: "d"},
|
||||
}},
|
||||
}
|
||||
if n := countWorkspaces(ws); n != 4 {
|
||||
t.Errorf("nested: got %d, want 4", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user