package store_test import ( "context" "testing" "atlas9.dev/c/core" "atlas9.dev/c/core/assert" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib" "atlas9.dev/c/demo/store" ) type groupTestHarness struct { Context context.Context Store iam.GroupStore NewTenants func(t *testing.T, n int) []core.ID NewUsers func(t *testing.T, n int) []core.ID AddMember func(t *testing.T, tenant, user core.ID) } func testGroupStore(t *testing.T, setup func(t *testing.T) groupTestHarness) { t.Run("Save and Get", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] in := &iam.Group{Tenant: tenant, Path: "eng", Name: "Engineering"} assert.Ok(t, h.Store.Save(ctx, in)) var got iam.Group err := h.Store.Get(ctx, tenant, "eng", &got) assert.Ok(t, err) assert.Eq(t, got, *in) }) t.Run("Save updates name", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Old"})) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "New"})) var got iam.Group err := h.Store.Get(ctx, tenant, "eng", &got) assert.Ok(t, err) assert.Eq(t, got.Name, "New") }) t.Run("Get not found", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] var got iam.Group err := h.Store.Get(h.Context, tenant, "nonexistent", &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Delete", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.Delete(ctx, tenant, "eng")) var got iam.Group err := h.Store.Get(ctx, tenant, "eng", &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("List", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "ops", Name: "Ops"})) var page core.Page[iam.Group] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) assert.Eq(t, page.Items[0].Path, core.Path("eng")) assert.Eq(t, page.Items[1].Path, core.Path("ops")) }) t.Run("List isolation", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenants[0], Path: "eng", Name: "Eng"})) var page core.Page[iam.Group] err := h.Store.List(ctx, tenants[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("AddMember and ListMembers", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] users := h.NewUsers(t, 2) h.AddMember(t, tenant, users[0]) h.AddMember(t, tenant, users[1]) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", users[0])) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", users[1])) var page core.Page[core.ID] err := h.Store.ListMembers(ctx, tenant, "eng", core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) }) t.Run("AddMember idempotent", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] h.AddMember(t, tenant, user) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", user)) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", user)) var page core.Page[core.ID] err := h.Store.ListMembers(ctx, tenant, "eng", core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 1) }) t.Run("RemoveMember", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] h.AddMember(t, tenant, user) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", user)) assert.Ok(t, h.Store.RemoveMember(ctx, tenant, "eng", user)) var page core.Page[core.ID] err := h.Store.ListMembers(ctx, tenant, "eng", core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListMemberGroups", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] h.AddMember(t, tenant, user) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "ops", Name: "Ops"})) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", user)) assert.Ok(t, h.Store.AddMember(ctx, tenant, "ops", user)) var page core.Page[iam.Group] err := h.Store.ListMemberGroups(ctx, user, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) }) t.Run("ListMemberGroups isolation", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] users := h.NewUsers(t, 2) h.AddMember(t, tenant, users[0]) h.AddMember(t, tenant, users[1]) assert.Ok(t, h.Store.Save(ctx, &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"})) assert.Ok(t, h.Store.AddMember(ctx, tenant, "eng", users[0])) var page core.Page[iam.Group] err := h.Store.ListMemberGroups(ctx, users[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("Save: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] err := h.Store.Save(t.Context(), &iam.Group{Tenant: tenant, Path: "eng", Name: "Eng"}) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("Get: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] var got iam.Group err := h.Store.Get(t.Context(), tenant, "eng", &got) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("Delete: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] err := h.Store.Delete(t.Context(), tenant, "eng") assert.Eq(t, err, iam.ErrForbidden) }) t.Run("List: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] var page core.Page[iam.Group] err := h.Store.List(t.Context(), tenant, core.PageReq{}, &page) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("AddMember: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] err := h.Store.AddMember(t.Context(), tenant, "eng", user) assert.Eq(t, err, iam.ErrForbidden) }) } func TestSqliteGroupStore(t *testing.T) { testGroupStore(t, func(t *testing.T) groupTestHarness { t.Helper() db := setupTestDB(t) tx, err := db.Begin() assert.Ok(t, err) t.Cleanup(func() { tx.Rollback() }) w := dbi.WrapTx(tx) guard := lib.ContextAccessGuard{} ctx := lib.PutAccess(t.Context(), lib.AdminAccess()) tenantStore := store.NewSqliteTenantStore(w, guard) userStore := store.NewSqliteUserStore(w, guard) memberStore := store.NewSqliteTenantMemberStore(w, guard) return groupTestHarness{ Context: ctx, Store: store.NewSqliteGroupStore(w, guard), NewTenants: func(t *testing.T, n int) []core.ID { t.Helper() ids := generateIDs(n) for _, id := range ids { err := tenantStore.Create(ctx, &iam.Tenant{ID: id, Name: "T"}) assert.Ok(t, err) } return ids }, NewUsers: func(t *testing.T, n int) []core.ID { t.Helper() ids := generateIDs(n) for _, id := range ids { err := userStore.Save(ctx, &iam.User{ID: id, Email: id.String() + "@test.com"}) assert.Ok(t, err) } return ids }, AddMember: func(t *testing.T, tenant, user core.ID) { t.Helper() err := memberStore.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: user}) assert.Ok(t, err) }, } }) }