package api_test import ( "net/http" "strings" "testing" "atlas9.dev/c/core" "atlas9.dev/c/core/assert" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/api" ) func TestTenantsApi_CreateAndGet(t *testing.T) { s := startServer(t) var saved api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "Acme"}}, &saved) assert.Eq(t, saved.Tenant.ID.IsEmpty(), false) var got api.Tenants_GetRes s.call(t, "/Tenants.Get", api.Tenants_GetReq{ID: saved.Tenant.ID}, &got) assert.Eq(t, got.Tenant.ID, saved.Tenant.ID) assert.Eq(t, got.Tenant.Name, "Acme") } func TestTenantsApi_CreateRejectsEmptyName(t *testing.T) { s := startServer(t) var body api.ErrorResponse res := s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{}}, &body) assert.Eq(t, res.StatusCode, http.StatusBadRequest) assert.Eq(t, body.Message, iam.ErrTenantNameEmpty.Error()) } func TestTenantsApi_CreateRejectsLongName(t *testing.T) { s := startServer(t) name := strings.Repeat("a", iam.TenantNameMaxLen+1) var body api.ErrorResponse res := s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: name}}, &body) assert.Eq(t, res.StatusCode, http.StatusBadRequest) assert.Eq(t, body.Message, iam.ErrTenantNameTooLong.Error()) } func TestTenantsApi_CreateRejectsInvalidJson(t *testing.T) { s := startServer(t) var body api.ErrorResponse res := s.call(t, "/Tenants.Create", []byte("not json"), &body) assert.Eq(t, res.StatusCode, http.StatusBadRequest) assert.Eq(t, body.Message, "invalid json: invalid character 'o' in literal null (expecting 'u')") } func TestTenantsApi_Delete(t *testing.T) { s := startServer(t) var saved api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "Acme"}}, &saved) s.call(t, "/Tenants.Delete", api.Tenants_DeleteReq{ID: saved.Tenant.ID}, nil) var body api.ErrorResponse res := s.call(t, "/Tenants.Get", api.Tenants_GetReq{ID: saved.Tenant.ID}, &body) assert.Eq(t, res.StatusCode, http.StatusNotFound) assert.Eq(t, body.Message, core.ErrNotFound.Error()) } func TestTenantsApi_GetNotFound(t *testing.T) { s := startServer(t) var body api.ErrorResponse res := s.call(t, "/Tenants.Get", api.Tenants_GetReq{ID: core.NewID("t")}, &body) assert.Eq(t, res.StatusCode, http.StatusNotFound) assert.Eq(t, body.Message, core.ErrNotFound.Error()) } func TestTenantsApi_List(t *testing.T) { s := startServer(t) for _, name := range []string{"Acme", "Globex", "Initech"} { s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: name}}, nil) } var list api.Tenants_ListRes s.call(t, "/Tenants.List", api.Tenants_ListReq{}, &list) // defaultTenant (seeded by startServer) + 3 created. assert.Eq(t, len(list.Page.Items), 4) } // A newly-registered user can create their own tenant because every // authenticated principal holds the built-in "authenticated" role, which // includes system-scoped CapTenantsCreate. func TestTenantsApi_Create_NewUser(t *testing.T) { s := startServer(t) s.logout(t) email, password := "new@test", "pass" s.call(t, "/Account.Register", api.Account_RegisterReq{ Email: email, Password: password, }, nil) token := fetchEmailVerificationToken(t, s) s.call(t, "/Account.Verify", api.Account_VerifyReq{Token: token}, nil) s.login(t, email, password) var saved api.Tenants_CreateRes res := s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "Acme"}}, &saved) assert.Eq(t, res.StatusCode, http.StatusOK) assert.Eq(t, saved.Tenant.ID.IsEmpty(), false) assert.Eq(t, saved.Tenant.Name, "Acme") } func TestTenantsApi_Create_Anonymous_Forbidden(t *testing.T) { s := startServer(t) s.logout(t) var body api.ErrorResponse res := s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "Acme"}}, &body) assert.Eq(t, res.StatusCode, http.StatusForbidden) assert.Eq(t, body.Message, iam.ErrForbidden.Error()) } // After creating a tenant, the creator is automatically the owner of that // tenant and can update it — the Create-implies-Update invariant. func TestTenantsApi_Update_AfterCreate(t *testing.T) { s := startServer(t) s.logout(t) email, password := "new@test", "pass" s.call(t, "/Account.Register", api.Account_RegisterReq{ Email: email, Password: password, }, nil) token := fetchEmailVerificationToken(t, s) s.call(t, "/Account.Verify", api.Account_VerifyReq{Token: token}, nil) s.login(t, email, password) var created api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "Old"}}, &created) updateRes := s.call(t, "/Tenants.Update", api.Tenants_UpdateReq{ Tenant: iam.Tenant{ID: created.Tenant.ID, Name: "New"}, }, nil) assert.Eq(t, updateRes.StatusCode, http.StatusOK) var got api.Tenants_GetRes s.call(t, "/Tenants.Get", api.Tenants_GetReq{ID: created.Tenant.ID}, &got) assert.Eq(t, got.Tenant.Name, "New") } func TestTenantsApi_Update_NotFound(t *testing.T) { s := startServer(t) var body api.ErrorResponse res := s.call(t, "/Tenants.Update", api.Tenants_UpdateReq{ Tenant: iam.Tenant{ID: core.NewID("t"), Name: "Nope"}, }, &body) assert.Eq(t, res.StatusCode, http.StatusNotFound) assert.Eq(t, body.Message, core.ErrNotFound.Error()) } func TestTenantsApi_Update_RejectsEmptyID(t *testing.T) { s := startServer(t) var body api.ErrorResponse res := s.call(t, "/Tenants.Update", api.Tenants_UpdateReq{Tenant: iam.Tenant{Name: "Nope"}}, &body) assert.Eq(t, res.StatusCode, http.StatusBadRequest) assert.Eq(t, body.Message, iam.ErrTenantIDEmpty.Error()) } func TestTenantsApi_Update_CrossTenant_Forbidden(t *testing.T) { s := startServer(t) var a, b api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "A"}}, &a) s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "B"}}, &b) s.seedUserInTenant(t, "scoped@test", "pw", a.Tenant.ID, iam.CapTenantsUpdate) s.login(t, "scoped@test", "pw") var body api.ErrorResponse res := s.call(t, "/Tenants.Update", api.Tenants_UpdateReq{ Tenant: iam.Tenant{ID: b.Tenant.ID, Name: "hacked"}, }, &body) assert.Eq(t, res.StatusCode, http.StatusForbidden) assert.Eq(t, body.Message, iam.ErrForbidden.Error()) } func TestTenantsApi_Get_TenantScope(t *testing.T) { s := startServer(t) // Default user (system-scoped) creates two tenants. var a, b api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "A"}}, &a) s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "B"}}, &b) // New user scoped to tenant A only. s.seedUserInTenant(t, "scoped@test", "pw", a.Tenant.ID, iam.CapTenantsRead) s.login(t, "scoped@test", "pw") // Own tenant: accessible. var got api.Tenants_GetRes res := s.call(t, "/Tenants.Get", api.Tenants_GetReq{ID: a.Tenant.ID}, &got) assert.Eq(t, res.StatusCode, http.StatusOK) assert.Eq(t, got.Tenant.ID, a.Tenant.ID) // Other tenant: forbidden. var body api.ErrorResponse res = s.call(t, "/Tenants.Get", api.Tenants_GetReq{ID: b.Tenant.ID}, &body) assert.Eq(t, res.StatusCode, http.StatusForbidden) assert.Eq(t, body.Message, iam.ErrForbidden.Error()) } func TestTenantsApi_Delete_CrossTenant_Forbidden(t *testing.T) { s := startServer(t) var a, b api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "A"}}, &a) s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "B"}}, &b) s.seedUserInTenant(t, "scoped@test", "pw", a.Tenant.ID, iam.CapTenantsDelete) s.login(t, "scoped@test", "pw") var body api.ErrorResponse res := s.call(t, "/Tenants.Delete", api.Tenants_DeleteReq{ID: b.Tenant.ID}, &body) assert.Eq(t, res.StatusCode, http.StatusForbidden) assert.Eq(t, body.Message, iam.ErrForbidden.Error()) } // List uses checkSystem, so even a user with tenant-scoped CapTenantsRead // cannot list tenants — only system-scoped grants pass the system check. func TestTenantsApi_List_TenantScope_Forbidden(t *testing.T) { s := startServer(t) var a api.Tenants_CreateRes s.call(t, "/Tenants.Create", api.Tenants_CreateReq{Tenant: iam.Tenant{Name: "A"}}, &a) s.seedUserInTenant(t, "scoped@test", "pw", a.Tenant.ID, iam.CapTenantsRead) s.login(t, "scoped@test", "pw") var body api.ErrorResponse res := s.call(t, "/Tenants.List", api.Tenants_ListReq{}, &body) assert.Eq(t, res.StatusCode, http.StatusForbidden) assert.Eq(t, body.Message, iam.ErrForbidden.Error()) }