package main import ( "testing" "atlas9.dev/c/core/iam" ) func saveRole(t *testing.T, url string, id iam.RoleID, name string, actions ...iam.Action) iam.Role { t.Helper() return mustRPC[iam.Role, iam.Role](t, url, "/roles/save", iam.Role{ ID: id, Name: name, Actions: actions, }) } func TestSaveRole(t *testing.T) { s := setupTestServer(t) role := saveRole(t, s.URL, "app.admin", "Admin", "users.read", "users.write") if role.ID != "app.admin" { t.Errorf("ID = %q, want %q", role.ID, "app.admin") } if role.Name != "Admin" { t.Errorf("name = %q, want %q", role.Name, "Admin") } if len(role.Actions) != 2 { t.Errorf("actions = %v, want 2", role.Actions) } } func TestSaveRoleCustomTenant(t *testing.T) { s := setupTestServer(t) // Custom role uses lowercased tenant ID as namespace. role := saveRole(t, s.URL, "01abc00000000000000000000z.editor", "Editor") if role.ID != "01abc00000000000000000000z.editor" { t.Errorf("ID = %q", role.ID) } } func TestSaveRoleNamespaceIsolation(t *testing.T) { s := setupTestServer(t) // Same local name in different namespaces must not conflict. saveRole(t, s.URL, "app.admin", "System Admin", "users.read") saveRole(t, s.URL, "01abc00000000000000000000z.admin", "Tenant Admin", "posts.read") sys := mustRPC[GetRoleReq, iam.Role](t, s.URL, "/roles/get", GetRoleReq{ID: "app.admin"}) if sys.Name != "System Admin" { t.Errorf("system role name = %q, want %q", sys.Name, "System Admin") } tenant := mustRPC[GetRoleReq, iam.Role](t, s.URL, "/roles/get", GetRoleReq{ID: "01abc00000000000000000000z.admin"}) if tenant.Name != "Tenant Admin" { t.Errorf("tenant role name = %q, want %q", tenant.Name, "Tenant Admin") } } func TestSaveRoleInvalidID(t *testing.T) { s := setupTestServer(t) cases := []string{ "", "no_namespace", // only one segment "has-hyphen.role", "ns..double", ".leading", } for _, id := range cases { r := rpcCall[iam.Role, iam.Role](t, s.URL, "/roles/save", iam.Role{ ID: iam.RoleID(id), Name: "Test", }) if r.Error == "" { t.Errorf("id %q: expected validation error", id) } } } func TestSaveRoleDuplicate(t *testing.T) { s := setupTestServer(t) saveRole(t, s.URL, "app.editor", "Editor", "posts.read") r := rpcCall[iam.Role, iam.Role](t, s.URL, "/roles/save", iam.Role{ ID: "app.editor", Name: "Editor Renamed", }) if r.Error != "" { t.Fatal(r.Error) } if r.Body.Name != "Editor" { t.Errorf("name = %q, want original %q", r.Body.Name, "Editor") } } func TestGetRole(t *testing.T) { s := setupTestServer(t) saveRole(t, s.URL, "app.editor", "Editor", "posts.read", "posts.write") got := mustRPC[GetRoleReq, iam.Role](t, s.URL, "/roles/get", GetRoleReq{ID: "app.editor"}) if got.Name != "Editor" { t.Errorf("name = %q, want %q", got.Name, "Editor") } if len(got.Actions) != 2 { t.Errorf("actions = %v, want 2", got.Actions) } } func TestGetRoleNotFound(t *testing.T) { s := setupTestServer(t) r := rpcCall[GetRoleReq, iam.Role](t, s.URL, "/roles/get", GetRoleReq{ID: "app.nope"}) if r.Error == "" { t.Error("expected error for not found") } } func TestDeleteRole(t *testing.T) { s := setupTestServer(t) saveRole(t, s.URL, "app.temp", "Temp") mustRPC[DeleteRoleReq, DeleteRoleRes](t, s.URL, "/roles/delete", DeleteRoleReq{ID: "app.temp"}) r := rpcCall[GetRoleReq, iam.Role](t, s.URL, "/roles/get", GetRoleReq{ID: "app.temp"}) if r.Error == "" { t.Error("expected error after delete") } }