From 88895a34e46760a17dfe77fe40b0456cde769204 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Tue, 12 May 2026 07:13:52 +0000 Subject: [PATCH 01/10] =?UTF-8?q?test(handlers/org=5Fimport):=20add=20org?= =?UTF-8?q?=5Fimport=5Fhelpers=5Ftest.go=20=E2=80=94=2024=20cases=20for=20?= =?UTF-8?q?pure=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover countWorkspaces, envRequirementKey, sanitizeEnvMembers, flattenAndSortRequirements, and collectOrgEnv. These helpers are the pure-logic core of the org-import preflight pipeline and have no sqlmock surface needed — all inputs are in-memory structs. Part of Phase 36 coverage-floor work. --- .../handlers/org_import_helpers_test.go | 561 ++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 workspace-server/internal/handlers/org_import_helpers_test.go diff --git a/workspace-server/internal/handlers/org_import_helpers_test.go b/workspace-server/internal/handlers/org_import_helpers_test.go new file mode 100644 index 00000000..e69ce497 --- /dev/null +++ b/workspace-server/internal/handlers/org_import_helpers_test.go @@ -0,0 +1,561 @@ +package handlers + +import ( + "testing" +) + +// ───────────────────────────────────────────────────────────────────────────── +// countWorkspaces tests +// ───────────────────────────────────────────────────────────────────────────── + +func TestCountWorkspaces_Empty(t *testing.T) { + got := countWorkspaces(nil) + if got != 0 { + t.Errorf("nil: got %d, want 0", got) + } + got = countWorkspaces([]OrgWorkspace{}) + if got != 0 { + t.Errorf("empty: got %d, want 0", got) + } +} + +func TestCountWorkspaces_Flat(t *testing.T) { + tree := []OrgWorkspace{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + } + got := countWorkspaces(tree) + if got != 3 { + t.Errorf("flat 3: got %d, want 3", got) + } +} + +func TestCountWorkspaces_Nested(t *testing.T) { + // root (1) + // / | \ (3 children) + // c1 c2 c3 + // | | + // g1 g2 (2 grandchildren) + tree := []OrgWorkspace{ + { + Name: "root", + Children: []OrgWorkspace{ + {Name: "child1", Children: []OrgWorkspace{{Name: "grandchild1"}}}, + {Name: "child2"}, + {Name: "child3", Children: []OrgWorkspace{{Name: "grandchild2"}}}, + }, + }, + } + got := countWorkspaces(tree) + if got != 6 { + t.Errorf("nested: got %d, want 6 (1 root + 3 children + 2 grandchildren)", got) + } +} + +func TestCountWorkspaces_DeepNesting(t *testing.T) { + // chain of 5 levels + deep := []OrgWorkspace{ + {Name: "L1", Children: []OrgWorkspace{ + {Name: "L2", Children: []OrgWorkspace{ + {Name: "L3", Children: []OrgWorkspace{ + {Name: "L4", Children: []OrgWorkspace{ + {Name: "L5"}, + }}, + }}, + }}, + }}, + } + got := countWorkspaces(deep) + if got != 5 { + t.Errorf("deep chain: got %d, want 5", got) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// envRequirementKey tests +// ───────────────────────────────────────────────────────────────────────────── + +func TestEnvRequirementKey_SingleMember(t *testing.T) { + got := envRequirementKey([]string{"API_KEY"}) + if got != "API_KEY" { + t.Errorf("single: got %q, want %q", got, "API_KEY") + } +} + +func TestEnvRequirementKey_TwoMembers_OrderInsensitive(t *testing.T) { + keyAB := envRequirementKey([]string{"A", "B"}) + keyBA := envRequirementKey([]string{"B", "A"}) + if keyAB != keyBA { + t.Errorf("order-insensitive: [A,B]=%q, [B,A]=%q — must match", keyAB, keyBA) + } +} + +func TestEnvRequirementKey_ThreeMembers_Sorted(t *testing.T) { + key := envRequirementKey([]string{"Z", "A", "M"}) + // Should be "A\x00M\x00Z" + want := "A\x00M\x00Z" + if key != want { + t.Errorf("three members sorted: got %q, want %q", key, want) + } +} + +func TestEnvRequirementKey_EmptyMembers(t *testing.T) { + got := envRequirementKey(nil) + if got != "" { + t.Errorf("nil: got %q, want empty", got) + } + got = envRequirementKey([]string{}) + if got != "" { + t.Errorf("empty: got %q, want empty", got) + } +} + +func TestEnvRequirementKey_DuplicateMembers(t *testing.T) { + // Duplicates should be preserved in sort; join still works + key := envRequirementKey([]string{"A", "A", "B"}) + want := "A\x00A\x00B" + if key != want { + t.Errorf("duplicates: got %q, want %q", key, want) + } +} + +func TestEnvRequirementKey_UsedForDedup(t *testing.T) { + // Real dedup case: {A,B} and {B,A} produce same key → dedup-eligible + // {A,B,C} produces a different key + keyAB := envRequirementKey([]string{"A", "B"}) + keyBA := envRequirementKey([]string{"B", "A"}) + keyABC := envRequirementKey([]string{"A", "B", "C"}) + if keyAB != keyBA { + t.Errorf("AB vs BA: keys must match for dedup") + } + if keyAB == keyABC { + t.Errorf("AB vs ABC: keys must differ") + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// sanitizeEnvMembers tests +// ───────────────────────────────────────────────────────────────────────────── +// envVarNamePattern = ^[A-Z][A-Z0-9_]{0,127}$ + +func TestSanitizeEnvMembers_AllValid(t *testing.T) { + members := []string{"API_KEY", "MY_VAR_2", "A"} + got, ok := sanitizeEnvMembers(members, "test") + if !ok { + t.Error("all valid: ok should be true") + } + if len(got) != len(members) { + t.Errorf("all valid: got %v, want %v", got, members) + } +} + +func TestSanitizeEnvMembers_SomeInvalid(t *testing.T) { + // Lowercase first char — invalid + members := []string{"API_KEY", "lowercase", "MY_VAR"} + got, ok := sanitizeEnvMembers(members, "test") + if !ok { + t.Error("one invalid: ok should be true (valid members remain)") + } + want := []string{"API_KEY", "MY_VAR"} + if len(got) != len(want) { + t.Errorf("one invalid: got %v, want %v", got, want) + } +} + +func TestSanitizeEnvMembers_AllInvalid_DropsAll(t *testing.T) { + members := []string{"lowercase", "123_START", ""} + got, ok := sanitizeEnvMembers(members, "test") + if ok { + t.Error("all invalid: ok should be false") + } + if len(got) != 0 { + t.Errorf("all invalid: got %v, want empty", got) + } +} + +func TestSanitizeEnvMembers_EmptyString_Skipped(t *testing.T) { + // Empty string is filtered but doesn't make ok=false + members := []string{"API_KEY", "", "MY_VAR"} + got, ok := sanitizeEnvMembers(members, "test") + if !ok { + t.Error("empty string in valid list: ok should be true") + } + if len(got) != 2 { + t.Errorf("empty string filtered: got %v, want [API_KEY, MY_VAR]", got) + } +} + +func TestSanitizeEnvMembers_MaxLength(t *testing.T) { + // 128 chars: valid (1 prefix + 127 more = 128) + valid := "A" + string(make([]byte, 127)) + got, ok := sanitizeEnvMembers([]string{valid}, "test") + if !ok { + t.Errorf("128 char valid: ok should be true, got %v", got) + } + // 129 chars: invalid + tooLong := "A" + string(make([]byte, 128)) + got, ok = sanitizeEnvMembers([]string{tooLong}, "test") + if ok { + t.Error("129 char invalid: ok should be false") + } +} + +func TestSanitizeEnvMembers_DigitsAndUnderscore(t *testing.T) { + valid := []string{"A1", "A_2", "_PRIVATE", "HTTP_200_OK", "ABC123"} + for _, v := range valid { + got, ok := sanitizeEnvMembers([]string{v}, "test") + if !ok { + t.Errorf("should be valid: %q", v) + } + if len(got) != 1 || got[0] != v { + t.Errorf("got %v, want [%q]", got, v) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// flattenAndSortRequirements tests +// ───────────────────────────────────────────────────────────────────────────── + +func TestFlattenAndSortRequirements_Empty(t *testing.T) { + got := flattenAndSortRequirements(map[string]EnvRequirement{}) + if len(got) != 0 { + t.Errorf("empty: got %d, want 0", len(got)) + } +} + +func TestFlattenAndSortRequirements_SingleFirst(t *testing.T) { + // Singles come before groups; within singles, alphabetical + reqs := map[string]EnvRequirement{ + envRequirementKey([]string{"ZETA"}): {Name: "ZETA"}, + envRequirementKey([]string{"ALPHA"}): {Name: "ALPHA"}, + } + got := flattenAndSortRequirements(reqs) + if len(got) != 2 { + t.Fatalf("got %d, want 2", len(got)) + } + if got[0].Name != "ALPHA" { + t.Errorf("first: got %q, want ALPHA", got[0].Name) + } + if got[1].Name != "ZETA" { + t.Errorf("second: got %q, want ZETA", got[1].Name) + } +} + +func TestFlattenAndSortRequirements_GroupsAfterSingles(t *testing.T) { + reqs := map[string]EnvRequirement{ + envRequirementKey([]string{"X"}): {Name: "X"}, // single + envRequirementKey([]string{"A", "B"}): {AnyOf: []string{"A", "B"}}, // group + } + got := flattenAndSortRequirements(reqs) + if len(got) != 2 { + t.Fatalf("got %d, want 2", len(got)) + } + // Single X comes before any group + if got[0].Name != "X" { + t.Errorf("first should be single X: got %+v", got[0]) + } + if len(got[1].AnyOf) != 2 { + t.Errorf("second should be group: got %+v", got[1]) + } +} + +func TestFlattenAndSortRequirements_GroupsSortedByMemberKey(t *testing.T) { + // Groups sorted by their member-key (same as envRequirementKey of AnyOf) + reqs := map[string]EnvRequirement{ + envRequirementKey([]string{"Z", "A"}): {AnyOf: []string{"Z", "A"}}, // key: A\x00Z + envRequirementKey([]string{"B", "C"}): {AnyOf: []string{"B", "C"}}, // key: B\x00C + } + got := flattenAndSortRequirements(reqs) + if len(got) != 2 { + t.Fatalf("got %d, want 2", len(got)) + } + // B\x00C < A\x00Z alphabetically, so B,C group first + if len(got[0].AnyOf) != 2 || got[0].AnyOf[0] != "B" { + t.Errorf("first group: got %+v, want [B,C]", got[0]) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// collectOrgEnv tests +// ───────────────────────────────────────────────────────────────────────────── + +func TestCollectOrgEnv_Empty(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(t *testing.T) { + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{Name: "API_KEY"}}, + } + req, rec := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("got %d required, want 1", len(req)) + } + if req[0].Name != "API_KEY" { + t.Errorf("name: got %q, want API_KEY", req[0].Name) + } + if len(rec) != 0 { + t.Errorf("recommended: got %d, want 0", len(rec)) + } +} + +func TestCollectOrgEnv_SingleRecommended(t *testing.T) { + tmpl := &OrgTemplate{ + RecommendedEnv: []EnvRequirement{{Name: "DEBUG"}}, + } + req, rec := collectOrgEnv(tmpl) + if len(req) != 0 { + t.Errorf("required: got %d, want 0", len(req)) + } + if len(rec) != 1 { + t.Fatalf("got %d recommended, want 1", len(rec)) + } + if rec[0].Name != "DEBUG" { + t.Errorf("name: got %q, want DEBUG", rec[0].Name) + } +} + +func TestCollectOrgEnv_AnyOfGroup(t *testing.T) { + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{AnyOf: []string{"AWS_KEY", "GCP_KEY", "AZURE_KEY"}}}, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("got %d, want 1", len(req)) + } + if len(req[0].AnyOf) != 3 { + t.Errorf("any_of members: got %v, want [AWS_KEY, GCP_KEY, AZURE_KEY]", req[0].AnyOf) + } +} + +func TestCollectOrgEnv_RequiredWinsOverRecommended(t *testing.T) { + // Same key in both tiers → required wins; recommended entry dropped + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{Name: "SHARED_KEY"}}, + RecommendedEnv: []EnvRequirement{{Name: "SHARED_KEY"}}, + } + req, rec := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("required: got %d, want 1", len(req)) + } + if req[0].Name != "SHARED_KEY" { + t.Errorf("required: got %q, want SHARED_KEY", req[0].Name) + } + if len(rec) != 0 { + t.Errorf("recommended should be empty (required wins): got %d entries", len(rec)) + } +} + +func TestCollectOrgEnv_InvalidNamesFiltered(t *testing.T) { + // "lowercase" and "" fail envVarNamePattern → silently dropped + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{AnyOf: []string{"VALID_KEY", "lowercase", ""}}}, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("invalid names filtered: got %d, want 1", len(req)) + } + if len(req[0].AnyOf) != 1 || req[0].AnyOf[0] != "VALID_KEY" { + t.Errorf("valid names kept: got %v", req[0].AnyOf) + } +} + +func TestCollectOrgEnv_GroupWithOneInvalid_KeepsRest(t *testing.T) { + // Mixed: one valid + one invalid → valid is kept + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{AnyOf: []string{"good_key", "also_good"}}}, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("got %d, want 1", len(req)) + } + if len(req[0].AnyOf) != 2 { + t.Errorf("kept both valid: got %v", req[0].AnyOf) + } +} + +func TestCollectOrgEnv_AllInvalidGroup_Dropped(t *testing.T) { + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{AnyOf: []string{"lowercase", ""}}}, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 0 { + t.Errorf("all-invalid group: got %d, want 0", len(req)) + } +} + +func TestCollectOrgEnv_RequiredSingleDominatesAnyOfGroup(t *testing.T) { + // Required: API_KEY (strict) + // Required: any_of [API_KEY, ALT_KEY] + // → the any_of group is redundant (API_KEY satisfies it already) + // → any_of group should be dropped from required + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{ + {Name: "API_KEY"}, + {AnyOf: []string{"API_KEY", "ALT_KEY"}}, + }, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("strict dominates group: got %d entries, want 1", len(req)) + } + if req[0].Name != "API_KEY" { + t.Errorf("strict: got %+v, want name=API_KEY", req[0]) + } +} + +func TestCollectOrgEnv_RequiredSingleDominatesRecommendedAnyOf(t *testing.T) { + // Required: FOO (strict) + // Recommended: any_of [FOO, BAR] + // → FOO is already required; the recommended any_of is redundant + // → recommended any_of should be dropped + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{Name: "FOO"}}, + RecommendedEnv: []EnvRequirement{{AnyOf: []string{"FOO", "BAR"}}}, + } + req, rec := collectOrgEnv(tmpl) + if len(req) != 1 || req[0].Name != "FOO" { + t.Errorf("required: got %+v", req) + } + if len(rec) != 0 { + t.Errorf("recommended any_of dominated by strict: got %d, want 0", len(rec)) + } +} + +func TestCollectOrgEnv_SameTierStrictDominatesGroup(t *testing.T) { + // Both in required: X (strict), any_of [X, Y] (group) + // Strict X makes the any_of redundant within the same tier + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{ + {Name: "X"}, + {AnyOf: []string{"X", "Y"}}, + }, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("got %d, want 1", len(req)) + } + if req[0].Name != "X" { + t.Errorf("strict dominates same-tier group: got %+v", req[0]) + } +} + +func TestCollectOrgEnv_WorkspaceLevel(t *testing.T) { + // Workspaces can also declare required/recommended env + tmpl := &OrgTemplate{ + Workspaces: []OrgWorkspace{ + { + Name: "Dev", + RequiredEnv: []EnvRequirement{{Name: "DEV_KEY"}}, + RecommendedEnv: []EnvRequirement{{Name: "DEV_TOOL"}}, + }, + }, + } + req, rec := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Fatalf("workspace required: got %d, want 1", len(req)) + } + if req[0].Name != "DEV_KEY" { + t.Errorf("workspace required: got %v", req[0]) + } + if len(rec) != 1 { + t.Fatalf("workspace recommended: got %d, want 1", len(rec)) + } + if rec[0].Name != "DEV_TOOL" { + t.Errorf("workspace recommended: got %v", rec[0]) + } +} + +func TestCollectOrgEnv_DeepNesting(t *testing.T) { + // Nested children also contribute env requirements + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{Name: "ORG_LEVEL"}}, + Workspaces: []OrgWorkspace{ + { + Name: "Root", + RequiredEnv: []EnvRequirement{{Name: "ROOT_LEVEL"}}, + Children: []OrgWorkspace{ + { + Name: "Child", + RequiredEnv: []EnvRequirement{{Name: "CHILD_LEVEL"}}, + Children: []OrgWorkspace{ + {Name: "GrandChild", RecommendedEnv: []EnvRequirement{{Name: "GRANDCHILD_TOOL"}}}, + }, + }, + }, + }, + }, + } + req, rec := collectOrgEnv(tmpl) + if len(req) != 3 { + t.Errorf("3 required levels: got %d: %+v", len(req), req) + } + if len(rec) != 1 { + t.Errorf("1 recommended: got %d: %+v", len(rec), rec) + } +} + +func TestCollectOrgEnv_DedupAcrossTiers(t *testing.T) { + // Same key declared at org level AND workspace level → deduped to 1 + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{{Name: "SHARED"}}, + Workspaces: []OrgWorkspace{ + {Name: "ws", RequiredEnv: []EnvRequirement{{Name: "SHARED"}}}, + }, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Errorf("dedup across tiers: got %d, want 1", len(req)) + } +} + +func TestCollectOrgEnv_DedupWithinGroup(t *testing.T) { + // Same key declared multiple times within required → deduped + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{ + {Name: "DUPE"}, + {Name: "DUPE"}, + }, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 1 { + t.Errorf("dedup within tier: got %d, want 1", len(req)) + } +} + +func TestCollectOrgEnv_MixedCasePreservesSort(t *testing.T) { + // Sort order: singles first (alpha), then groups (by member-key) + tmpl := &OrgTemplate{ + RequiredEnv: []EnvRequirement{ + {Name: "ZETA"}, + {Name: "ALPHA"}, + {AnyOf: []string{"B", "A"}}, // key: A\x00B + {AnyOf: []string{"Y", "X"}}, // key: X\x00Y + }, + } + req, _ := collectOrgEnv(tmpl) + if len(req) != 4 { + t.Fatalf("got %d, want 4", len(req)) + } + // Singles first + if req[0].Name != "ALPHA" { + t.Errorf("single ALPHA first: got %+v", req[0]) + } + if req[1].Name != "ZETA" { + t.Errorf("single ZETA second: got %+v", req[1]) + } + // Groups after singles; A,B (key A\x00B) < X,Y (key X\x00Y) + if len(req[2].AnyOf) != 2 { + t.Errorf("third should be group: got %+v", req[2]) + } + if req[2].AnyOf[0] != "B" { // "B" is first alphabetically in [A,B] + t.Errorf("A,B group should come first: got %+v", req[2]) + } +} + From 9b54adc4f9c982eddddafd5c8b8800d1554338dc Mon Sep 17 00:00:00 2001 From: core-devops Date: Tue, 12 May 2026 20:52:11 +0000 Subject: [PATCH 02/10] ci: rerun after mc#724 all-required fix lands From 07ac7f7e483f722b91e17de502ae6c6f867b81eb Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Tue, 12 May 2026 21:16:22 +0000 Subject: [PATCH 03/10] ci: rerun after concurrency-block clear From 410400d3c9db40d3df2982edd486e4f29b397ea8 Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Tue, 12 May 2026 21:25:10 +0000 Subject: [PATCH 04/10] ci: clean-slate rerun From 758a99d4a6ac8eac5df050923215e943d808698b Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Tue, 12 May 2026 21:30:19 +0000 Subject: [PATCH 05/10] ci: post-restart rerun From d95ab4df1dafc5319b22af2f8eaec069b250f663 Mon Sep 17 00:00:00 2001 From: core-lead Date: Tue, 12 May 2026 21:35:06 +0000 Subject: [PATCH 06/10] ci: clean-slate rerun v2 From 56dfe30f9d729bbd3210a2469420aa6e58bd6441 Mon Sep 17 00:00:00 2001 From: core-lead Date: Tue, 12 May 2026 21:44:36 +0000 Subject: [PATCH 07/10] ci: global-zombie-purge rerun From b4b675b540ca9644708700cad6ae228d72978ee2 Mon Sep 17 00:00:00 2001 From: core-lead Date: Tue, 12 May 2026 21:48:25 +0000 Subject: [PATCH 08/10] ci: post-full-purge rerun From e097f8f91d571ce73a337e6d327b3f2ea823ee88 Mon Sep 17 00:00:00 2001 From: core-devops Date: Tue, 12 May 2026 22:07:22 +0000 Subject: [PATCH 09/10] ci: post-purge rerun From 8aa409211ce1477a1156a88a1bb47556ef0769fa Mon Sep 17 00:00:00 2001 From: Molecule AI Core-Security Date: Tue, 12 May 2026 18:45:53 -0700 Subject: [PATCH 10/10] fix(test): correct org_import_helpers_test logic errors and remove duplicates Remove TestCollectOrgEnv_Empty and TestCollectOrgEnv_RequiredWinsOverRecommended which are already declared in org_test.go. Fix TestSanitizeEnvMembers_MaxLength to use printable chars instead of null bytes, fix TestSanitizeEnvMembers_DigitsAndUnderscore to drop leading-underscore names that fail ^[A-Z] regex, fix TestFlattenAndSortRequirements_GroupsSortedByMemberKey assertion order (A < B), and fix TestCollectOrgEnv_GroupWithOneInvalid_KeepsRest to use valid/invalid names that the sanitizer will actually filter. Co-Authored-By: Claude Sonnet 4.6 --- .../handlers/org_import_helpers_test.go | 56 ++++++------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/workspace-server/internal/handlers/org_import_helpers_test.go b/workspace-server/internal/handlers/org_import_helpers_test.go index e69ce497..b4702e85 100644 --- a/workspace-server/internal/handlers/org_import_helpers_test.go +++ b/workspace-server/internal/handlers/org_import_helpers_test.go @@ -1,6 +1,7 @@ package handlers import ( + "strings" "testing" ) @@ -187,14 +188,14 @@ func TestSanitizeEnvMembers_EmptyString_Skipped(t *testing.T) { } func TestSanitizeEnvMembers_MaxLength(t *testing.T) { - // 128 chars: valid (1 prefix + 127 more = 128) - valid := "A" + string(make([]byte, 127)) + // 128 chars: valid (1 prefix + 127 more = 128, all uppercase) + valid := "A" + strings.Repeat("B", 127) got, ok := sanitizeEnvMembers([]string{valid}, "test") if !ok { t.Errorf("128 char valid: ok should be true, got %v", got) } - // 129 chars: invalid - tooLong := "A" + string(make([]byte, 128)) + // 129 chars: invalid (exceeds {0,127} suffix in regex) + tooLong := "A" + strings.Repeat("B", 128) got, ok = sanitizeEnvMembers([]string{tooLong}, "test") if ok { t.Error("129 char invalid: ok should be false") @@ -202,7 +203,8 @@ func TestSanitizeEnvMembers_MaxLength(t *testing.T) { } func TestSanitizeEnvMembers_DigitsAndUnderscore(t *testing.T) { - valid := []string{"A1", "A_2", "_PRIVATE", "HTTP_200_OK", "ABC123"} + // regex ^[A-Z][A-Z0-9_]{0,127}$ — first char must be A-Z, not underscore + valid := []string{"A1", "A_2", "HTTP_200_OK", "ABC123"} for _, v := range valid { got, ok := sanitizeEnvMembers([]string{v}, "test") if !ok { @@ -262,7 +264,8 @@ func TestFlattenAndSortRequirements_GroupsAfterSingles(t *testing.T) { } func TestFlattenAndSortRequirements_GroupsSortedByMemberKey(t *testing.T) { - // Groups sorted by their member-key (same as envRequirementKey of AnyOf) + // Groups sorted by their member-key (envRequirementKey sorts AnyOf members). + // {Z,A} → key "A\x00Z"; {B,C} → key "B\x00C". "A..." < "B..." → A,Z group first. reqs := map[string]EnvRequirement{ envRequirementKey([]string{"Z", "A"}): {AnyOf: []string{"Z", "A"}}, // key: A\x00Z envRequirementKey([]string{"B", "C"}): {AnyOf: []string{"B", "C"}}, // key: B\x00C @@ -271,9 +274,9 @@ func TestFlattenAndSortRequirements_GroupsSortedByMemberKey(t *testing.T) { if len(got) != 2 { t.Fatalf("got %d, want 2", len(got)) } - // B\x00C < A\x00Z alphabetically, so B,C group first - if len(got[0].AnyOf) != 2 || got[0].AnyOf[0] != "B" { - t.Errorf("first group: got %+v, want [B,C]", got[0]) + // A\x00Z < B\x00C alphabetically, so the A,Z group sorts first + if len(got[0].AnyOf) != 2 || got[0].AnyOf[0] != "Z" { + t.Errorf("first group: got %+v, want [Z,A] (key A\\x00Z sorts before B\\x00C)", got[0]) } } @@ -281,14 +284,6 @@ func TestFlattenAndSortRequirements_GroupsSortedByMemberKey(t *testing.T) { // collectOrgEnv tests // ───────────────────────────────────────────────────────────────────────────── -func TestCollectOrgEnv_Empty(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(t *testing.T) { tmpl := &OrgTemplate{ RequiredEnv: []EnvRequirement{{Name: "API_KEY"}}, @@ -334,24 +329,6 @@ func TestCollectOrgEnv_AnyOfGroup(t *testing.T) { } } -func TestCollectOrgEnv_RequiredWinsOverRecommended(t *testing.T) { - // Same key in both tiers → required wins; recommended entry dropped - tmpl := &OrgTemplate{ - RequiredEnv: []EnvRequirement{{Name: "SHARED_KEY"}}, - RecommendedEnv: []EnvRequirement{{Name: "SHARED_KEY"}}, - } - req, rec := collectOrgEnv(tmpl) - if len(req) != 1 { - t.Fatalf("required: got %d, want 1", len(req)) - } - if req[0].Name != "SHARED_KEY" { - t.Errorf("required: got %q, want SHARED_KEY", req[0].Name) - } - if len(rec) != 0 { - t.Errorf("recommended should be empty (required wins): got %d entries", len(rec)) - } -} - func TestCollectOrgEnv_InvalidNamesFiltered(t *testing.T) { // "lowercase" and "" fail envVarNamePattern → silently dropped tmpl := &OrgTemplate{ @@ -367,16 +344,17 @@ func TestCollectOrgEnv_InvalidNamesFiltered(t *testing.T) { } func TestCollectOrgEnv_GroupWithOneInvalid_KeepsRest(t *testing.T) { - // Mixed: one valid + one invalid → valid is kept + // Mixed: one valid + one invalid → valid member is kept, invalid dropped + // regex requires ^[A-Z][A-Z0-9_]* — lowercase names are invalid tmpl := &OrgTemplate{ - RequiredEnv: []EnvRequirement{{AnyOf: []string{"good_key", "also_good"}}}, + RequiredEnv: []EnvRequirement{{AnyOf: []string{"GOOD_KEY", "lowercase_invalid"}}}, } req, _ := collectOrgEnv(tmpl) if len(req) != 1 { t.Fatalf("got %d, want 1", len(req)) } - if len(req[0].AnyOf) != 2 { - t.Errorf("kept both valid: got %v", req[0].AnyOf) + if len(req[0].AnyOf) != 1 || req[0].AnyOf[0] != "GOOD_KEY" { + t.Errorf("kept valid member: got %v, want [GOOD_KEY]", req[0].AnyOf) } }