package main import ( "testing" "atlas9.dev/c/core" "atlas9.dev/c/core/iam" ) func saveGroup(t *testing.T, url string, path core.Path, name string) iam.Group { t.Helper() return mustRPC[iam.Group, iam.Group](t, url, "/groups/save", iam.Group{ Path: path, Name: name, }) } func TestSaveGroup(t *testing.T) { s := setupTestServer(t) g := saveGroup(t, s.URL, "app.engineering", "Engineering") if g.ID.IsZero() { t.Error("expected non-zero group ID") } if g.Name != "Engineering" { t.Errorf("name = %q, want %q", g.Name, "Engineering") } } func TestSaveGroupInvalidPath(t *testing.T) { s := setupTestServer(t) cases := []string{ "", ".leading", "trailing.", "double..dot", "has-hyphen", "has space", } for _, path := range cases { r := rpcCall[iam.Group, iam.Group](t, s.URL, "/groups/save", iam.Group{ Path: core.Path(path), Name: "Test", }) if r.Status != 400 { t.Errorf("path %q: expected status 400, got %d", path, r.Status) } } } func TestSaveGroupDuplicatePath(t *testing.T) { s := setupTestServer(t) saveGroup(t, s.URL, "app.engineering", "Engineering") r := rpcCall[iam.Group, iam.Group](t, s.URL, "/groups/save", iam.Group{ Path: "app.engineering", Name: "Engineering Again", }) if r.Error != "" { t.Fatal(r.Error) } } func TestSaveGroupParentMissing(t *testing.T) { s := setupTestServer(t) r := rpcCall[iam.Group, iam.Group](t, s.URL, "/groups/save", iam.Group{ Path: "app.engineering.backend", Name: "Backend", }) if r.Error == "" { t.Error("expected error when parent does not exist") } } func TestSaveGroupNested(t *testing.T) { s := setupTestServer(t) saveGroup(t, s.URL, "app.engineering", "Engineering") child := saveGroup(t, s.URL, "app.engineering.backend", "Backend") if child.Path != "app.engineering.backend" { t.Errorf("path = %q, want %q", child.Path, "app.engineering.backend") } } func TestGetGroup(t *testing.T) { s := setupTestServer(t) created := saveGroup(t, s.URL, "app.engineering", "Engineering") got := mustRPC[GetGroupReq, iam.Group](t, s.URL, "/groups/get", GetGroupReq{ID: created.ID}) if got.Name != "Engineering" { t.Errorf("name = %q, want %q", got.Name, "Engineering") } } func TestGetGroupByPath(t *testing.T) { s := setupTestServer(t) created := saveGroup(t, s.URL, "app.engineering", "Engineering") got := mustRPC[GetGroupByPathReq, iam.Group](t, s.URL, "/groups/get-by-path", GetGroupByPathReq{ Path: "app.engineering", }) if !got.ID.Equal(created.ID) { t.Errorf("id = %s, want %s", got.ID, created.ID) } } func TestGetGroupNotFound(t *testing.T) { s := setupTestServer(t) r := rpcCall[GetGroupReq, iam.Group](t, s.URL, "/groups/get", GetGroupReq{ID: core.NewID()}) if r.Error == "" { t.Error("expected error for not found") } } func TestDeleteGroup(t *testing.T) { s := setupTestServer(t) g := saveGroup(t, s.URL, "app.engineering", "Engineering") mustRPC[DeleteGroupReq, DeleteGroupRes](t, s.URL, "/groups/delete", DeleteGroupReq{ID: g.ID}) r := rpcCall[GetGroupReq, iam.Group](t, s.URL, "/groups/get", GetGroupReq{ID: g.ID}) if r.Error == "" { t.Error("expected error after delete") } } func TestDeleteGroupWithChildren(t *testing.T) { s := setupTestServer(t) parent := saveGroup(t, s.URL, "app.engineering", "Engineering") saveGroup(t, s.URL, "app.engineering.backend", "Backend") r := rpcCall[DeleteGroupReq, DeleteGroupRes](t, s.URL, "/groups/delete", DeleteGroupReq{ID: parent.ID}) if r.Error == "" { t.Error("expected error deleting group with children") } } func TestDeleteGroupWithUnderscorePath(t *testing.T) { s := setupTestServer(t) // Create a group whose path contains underscores. // The LIKE query must escape _ to avoid wildcard matching. parent := saveGroup(t, s.URL, "app.backend_api", "Backend API") // No children — delete should succeed. mustRPC[DeleteGroupReq, DeleteGroupRes](t, s.URL, "/groups/delete", DeleteGroupReq{ID: parent.ID}) } func TestDeleteGroupWithUnderscoreHasChildren(t *testing.T) { s := setupTestServer(t) parent := saveGroup(t, s.URL, "app.backend_api", "Backend API") saveGroup(t, s.URL, "app.backend_api.v2", "V2") r := rpcCall[DeleteGroupReq, DeleteGroupRes](t, s.URL, "/groups/delete", DeleteGroupReq{ID: parent.ID}) if r.Error == "" { t.Error("expected error deleting group with children") } } func TestNamespaceIsolation(t *testing.T) { s := setupTestServer(t) tenant1 := core.NewID().String() tenant2 := core.NewID().String() // Different namespaces can have the same leaf path. g1 := saveGroup(t, s.URL, core.Path(tenant1+".engineering"), "T1 Engineering") g2 := saveGroup(t, s.URL, core.Path(tenant2+".engineering"), "T2 Engineering") if g1.ID.Equal(g2.ID) { t.Error("expected different IDs for different namespaces") } got1 := mustRPC[GetGroupByPathReq, iam.Group](t, s.URL, "/groups/get-by-path", GetGroupByPathReq{ Path: core.Path(tenant1 + ".engineering"), }) if got1.Name != "T1 Engineering" { t.Errorf("tenant1 name = %q, want %q", got1.Name, "T1 Engineering") } got2 := mustRPC[GetGroupByPathReq, iam.Group](t, s.URL, "/groups/get-by-path", GetGroupByPathReq{ Path: core.Path(tenant2 + ".engineering"), }) if got2.Name != "T2 Engineering" { t.Errorf("tenant2 name = %q, want %q", got2.Name, "T2 Engineering") } } func TestListGroupsByNamespace(t *testing.T) { s := setupTestServer(t) // Create groups in different namespaces saveGroup(t, s.URL, "app.engineering", "Engineering") saveGroup(t, s.URL, "app.design", "Design") ns := core.NewID().String() saveGroup(t, s.URL, core.Path(ns+".team1"), "Team 1") // List by "app" namespace res := mustRPC[ListGroupsByNamespaceReq, ListGroupsRes](t, s.URL, "/groups/list-by-namespace", ListGroupsByNamespaceReq{Namespace: "app"}) if len(res.Items) != 2 { t.Errorf("expected 2 groups in app namespace, got %d", len(res.Items)) } // List by tenant namespace res = mustRPC[ListGroupsByNamespaceReq, ListGroupsRes](t, s.URL, "/groups/list-by-namespace", ListGroupsByNamespaceReq{Namespace: ns}) if len(res.Items) != 1 { t.Errorf("expected 1 group in tenant namespace, got %d", len(res.Items)) } // List by nonexistent namespace res = mustRPC[ListGroupsByNamespaceReq, ListGroupsRes](t, s.URL, "/groups/list-by-namespace", ListGroupsByNamespaceReq{Namespace: "nonexistent"}) if len(res.Items) != 0 { t.Errorf("expected 0 groups in nonexistent namespace, got %d", len(res.Items)) } }