package store_test import ( "context" "slices" "strings" "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 tenantMemberTestHarness struct { Context context.Context Store iam.TenantMemberStore NewTenants func(t *testing.T, n int) []core.ID NewUsers func(t *testing.T, n int) []core.ID } // testTenantMemberStore runs a behavioral test suite against a TenantMemberStore. func testTenantMemberStore(t *testing.T, setup func(t *testing.T) tenantMemberTestHarness) { t.Run("Create and Get", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] in := iam.TenantMember{Tenant: tenant, UserID: user} assert.Ok(t, h.Store.Create(ctx, in)) var out iam.TenantMember err := h.Store.Get(ctx, tenant, user, &out) assert.Ok(t, err) assert.Eq(t, in, out) }) t.Run("Create as owner", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] in := iam.TenantMember{Tenant: tenant, UserID: user, Owner: true} assert.Ok(t, h.Store.Create(ctx, in)) var out iam.TenantMember err := h.Store.Get(ctx, tenant, user, &out) assert.Ok(t, err) assert.Eq(t, in, out) }) t.Run("Create rejects duplicate", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: user})) err := h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: user}) assert.Eq(t, err, iam.ErrAlreadyExists) }) t.Run("Remove", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: user})) assert.Ok(t, h.Store.Remove(ctx, tenant, user)) var got iam.TenantMember err := h.Store.Get(ctx, tenant, user, &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Remove cannot remove last owner", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: user, Owner: true})) err := h.Store.Remove(ctx, tenant, user) assert.Eq(t, err, iam.ErrLastOwner) }) t.Run("Remove owner when another exists", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] users := h.NewUsers(t, 2) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: users[0], Owner: true})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: users[1], Owner: true})) assert.Ok(t, h.Store.Remove(ctx, tenant, users[0])) var got iam.TenantMember err := h.Store.Get(ctx, tenant, users[0], &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Remove user never added", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] err := h.Store.Remove(ctx, tenant, user) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Create nonexistent user", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] m := iam.TenantMember{Tenant: tenant, UserID: core.NewID("t")} assert.Eq(t, iam.ErrUserNotFound, h.Store.Create(ctx, m)) }) t.Run("List", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: user})) var page core.Page[iam.TenantMember] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 1) }) t.Run("List pagination", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] users := h.NewUsers(t, 210) for _, id := range users { assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: id})) } var page core.Page[iam.TenantMember] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 100) assert.Eq(t, page.Items[0].UserID, users[0]) assert.Eq(t, page.Items[99].UserID, users[99]) var page2 core.Page[iam.TenantMember] err = h.Store.List(ctx, tenant, core.PageReq{Cursor: page.Cursor}, &page2) assert.Ok(t, err) assert.Eq(t, len(page2.Items), 100) assert.Eq(t, page2.Items[0].UserID, users[100]) assert.Eq(t, page2.Items[99].UserID, users[199]) var page3 core.Page[iam.TenantMember] err = h.Store.List(ctx, tenant, core.PageReq{Cursor: page2.Cursor}, &page3) assert.Ok(t, err) assert.Eq(t, len(page3.Items), 10) assert.Eq(t, page3.Cursor, "") assert.Eq(t, page3.Items[0].UserID, users[200]) assert.Eq(t, page3.Items[9].UserID, users[209]) }) t.Run("List tenant isolation", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: user})) var page core.Page[iam.TenantMember] err := h.Store.List(ctx, tenants[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("Get tenant isolation", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: user})) var got iam.TenantMember err := h.Store.Get(ctx, tenants[1], user, &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Create to nonexistent tenant", func(t *testing.T) { h := setup(t) ctx := h.Context user := h.NewUsers(t, 1)[0] err := h.Store.Create(ctx, iam.TenantMember{Tenant: core.NewID("t"), UserID: user}) assert.Eq(t, err, iam.ErrTenantNotFound) }) t.Run("Get from nonexistent tenant", func(t *testing.T) { h := setup(t) ctx := h.Context user := h.NewUsers(t, 1)[0] var got iam.TenantMember err := h.Store.Get(ctx, core.NewID("t"), user, &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Remove from nonexistent tenant", func(t *testing.T) { h := setup(t) ctx := h.Context user := h.NewUsers(t, 1)[0] err := h.Store.Remove(ctx, core.NewID("t"), user) assert.Eq(t, err, core.ErrNotFound) }) t.Run("List nonexistent tenant", func(t *testing.T) { h := setup(t) ctx := h.Context var page core.Page[iam.TenantMember] err := h.Store.List(ctx, core.NewID("t"), core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListByUser", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) user := h.NewUsers(t, 1)[0] assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: user})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: user})) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, user, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) assert.Eq(t, page.Items[0].Tenant, tenants[0]) assert.Eq(t, page.Items[1].Tenant, tenants[1]) }) t.Run("ListByUser isolation", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] users := h.NewUsers(t, 2) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenant, UserID: users[0]})) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListByUser isolation 2", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) users := h.NewUsers(t, 2) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: users[1]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[1]})) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) assert.Eq(t, page.Items[0].Tenant, tenants[0]) assert.Eq(t, page.Items[1].Tenant, tenants[1]) var page2 core.Page[iam.TenantMember] err = h.Store.ListByUser(ctx, users[0], core.PageReq{}, &page2) assert.Ok(t, err) assert.Eq(t, len(page2.Items), 1) assert.Eq(t, page2.Items[0].Tenant, tenants[0]) }) t.Run("ListByUser no memberships", func(t *testing.T) { h := setup(t) ctx := h.Context user := h.NewUsers(t, 1)[0] var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, user, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListByUser: caller can see their own tenants", func(t *testing.T) { h := setup(t) ctx := h.Context adminAccess := lib.Access{} adminAccess.GrantSystem(iam.CapTenantMembersCreate) ctx = lib.PutAccess(ctx, adminAccess) tenants := h.NewTenants(t, 2) users := h.NewUsers(t, 2) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[1]})) userAccess := lib.Access{} ctx = lib.PutAccess(ctx, userAccess) // the caller is users[0] ctx = iam.PutPrincipal(ctx, iam.Principal{Subject: users[0].String()}) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[0], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) }) t.Run("ListByUser: caller can see tenants they have access to", func(t *testing.T) { h := setup(t) adminAccess := lib.Access{} adminAccess.GrantSystem(iam.CapTenantMembersCreate) ctx := lib.PutAccess(t.Context(), adminAccess) tenants := h.NewTenants(t, 3) users := h.NewUsers(t, 3) // The test here is that users[0] is in tenant 0, 1, and 2. // The caller (users[2]) has iam.CapTenantMembersRead on tenant 0 and 1. // The caller lists tenants by user for users[0], and they can only see // the tenants they have access to: 0 and 1, and not 2. assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[1]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[2], UserID: users[0]})) userAccess := lib.Access{} userAccess.Grant(iam.CapTenantMembersRead, tenants[0], "") userAccess.Grant(iam.CapTenantMembersRead, tenants[1], "") ctx = lib.PutAccess(ctx, userAccess) ctx = iam.PutPrincipal(ctx, iam.Principal{Subject: users[2].String()}) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[0], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) assert.Eq(t, page.Items[0].Tenant, tenants[0]) assert.Eq(t, page.Items[1].Tenant, tenants[1]) }) t.Run("ListByUser: filtered pagination across pages", func(t *testing.T) { h := setup(t) adminAccess := lib.Access{} adminAccess.GrantSystem(iam.CapTenantMembersCreate) ctx := lib.PutAccess(t.Context(), adminAccess) // users[0] is in 5 tenants. The caller (users[1]) has access to all 5. // With a page limit of 2, pagination should yield 3 pages: 2, 2, 1. tenants := h.NewTenants(t, 5) users := h.NewUsers(t, 2) for _, tid := range tenants { assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tid, UserID: users[0]})) } callerAccess := lib.Access{} for _, tid := range tenants { callerAccess.Grant(iam.CapTenantMembersRead, tid, "") } ctx = lib.PutAccess(ctx, callerAccess) ctx = iam.PutPrincipal(ctx, iam.Principal{Subject: users[1].String()}) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[0], core.PageReq{Limit: 2}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) assert.Eq(t, page.Items[0].Tenant, tenants[0]) assert.Eq(t, page.Items[1].Tenant, tenants[1]) var page2 core.Page[iam.TenantMember] err = h.Store.ListByUser(ctx, users[0], core.PageReq{Limit: 2, Cursor: page.Cursor}, &page2) assert.Ok(t, err) assert.Eq(t, len(page2.Items), 2) assert.Eq(t, page2.Items[0].Tenant, tenants[2]) assert.Eq(t, page2.Items[1].Tenant, tenants[3]) var page3 core.Page[iam.TenantMember] err = h.Store.ListByUser(ctx, users[0], core.PageReq{Limit: 2, Cursor: page2.Cursor}, &page3) assert.Ok(t, err) assert.Eq(t, len(page3.Items), 1) assert.Eq(t, page3.Cursor, "") assert.Eq(t, page3.Items[0].Tenant, tenants[4]) }) t.Run("ListByUser: caller cannot list other users", func(t *testing.T) { h := setup(t) ctx := t.Context() adminAccess := lib.Access{} adminAccess.GrantSystem(iam.CapTenantMembersCreate) ctx = lib.PutAccess(ctx, adminAccess) tenants := h.NewTenants(t, 2) users := h.NewUsers(t, 2) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[1]})) userAccess := lib.Access{} ctx = lib.PutAccess(ctx, userAccess) // the caller is users[0], query is users[1] ctx = iam.PutPrincipal(ctx, iam.Principal{Subject: users[0].String()}) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListByUser: system caller can see all tenants + users", func(t *testing.T) { h := setup(t) ctx := t.Context() adminAccess := lib.Access{} adminAccess.GrantSystem(iam.CapTenantMembersCreate) adminAccess.GrantSystem(iam.CapTenantMembersRead) ctx = lib.PutAccess(ctx, adminAccess) tenants := h.NewTenants(t, 2) users := h.NewUsers(t, 2) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[0], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[0]})) assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: tenants[1], UserID: users[1]})) ctx = iam.PutPrincipal(ctx, iam.Principal{Subject: "system.svc"}) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, users[0], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) var page2 core.Page[iam.TenantMember] err = h.Store.ListByUser(ctx, users[1], core.PageReq{}, &page2) assert.Ok(t, err) assert.Eq(t, len(page2.Items), 1) }) t.Run("ListByUser pagination", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 210) user := h.NewUsers(t, 1)[0] for _, id := range tenants { assert.Ok(t, h.Store.Create(ctx, iam.TenantMember{Tenant: id, UserID: user})) } var page core.Page[iam.TenantMember] err := h.Store.ListByUser(ctx, user, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 100) assert.Eq(t, page.Items[0].Tenant, tenants[0]) assert.Eq(t, page.Items[99].Tenant, tenants[99]) var page2 core.Page[iam.TenantMember] err = h.Store.ListByUser(ctx, user, core.PageReq{Cursor: page.Cursor}, &page2) assert.Ok(t, err) assert.Eq(t, len(page2.Items), 100) assert.Eq(t, page2.Items[0].Tenant, tenants[100]) assert.Eq(t, page2.Items[99].Tenant, tenants[199]) var page3 core.Page[iam.TenantMember] err = h.Store.ListByUser(ctx, user, core.PageReq{Cursor: page2.Cursor}, &page3) assert.Ok(t, err) assert.Eq(t, len(page3.Items), 10) assert.Eq(t, page3.Cursor, "") assert.Eq(t, page3.Items[0].Tenant, tenants[200]) assert.Eq(t, page3.Items[9].Tenant, tenants[209]) }) t.Run("Create: no access", func(t *testing.T) { h := setup(t) err := h.Store.Create(t.Context(), iam.TenantMember{}) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("Get: no access", func(t *testing.T) { h := setup(t) var got iam.TenantMember err := h.Store.Get(t.Context(), core.ID{}, core.ID{}, &got) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("Remove: no access", func(t *testing.T) { h := setup(t) err := h.Store.Remove(t.Context(), core.ID{}, core.ID{}) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("List: no access", func(t *testing.T) { h := setup(t) var page core.Page[iam.TenantMember] err := h.Store.List(t.Context(), core.ID{}, core.PageReq{}, &page) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("Create with empty tenant", func(t *testing.T) { h := setup(t) user := h.NewUsers(t, 1)[0] in := iam.TenantMember{Tenant: core.ID{}, UserID: user} assert.Eq(t, iam.ErrTenantNotFound, h.Store.Create(h.Context, in)) }) t.Run("Create with empty user", func(t *testing.T) { h := setup(t) tenants := h.NewTenants(t, 1) in := iam.TenantMember{Tenant: tenants[0], UserID: core.ID{}} assert.Eq(t, iam.ErrUserNotFound, h.Store.Create(h.Context, in)) }) t.Run("Remove with empty tenant", func(t *testing.T) { h := setup(t) users := h.NewUsers(t, 1) err := h.Store.Remove(h.Context, core.ID{}, users[0]) // TODO Create distinguishes between user vs tenant not found assert.Eq(t, err, core.ErrNotFound) }) t.Run("Remove with empty user", func(t *testing.T) { h := setup(t) tenants := h.NewTenants(t, 1) err := h.Store.Remove(h.Context, tenants[0], core.ID{}) // TODO Create distinguishes between user vs tenant not found assert.Eq(t, err, core.ErrNotFound) }) t.Run("Get with empty tenant", func(t *testing.T) { h := setup(t) users := h.NewUsers(t, 1) var got iam.TenantMember err := h.Store.Get(h.Context, core.ID{}, users[0], &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("Get with empty user", func(t *testing.T) { h := setup(t) tenants := h.NewTenants(t, 1) var got iam.TenantMember err := h.Store.Get(h.Context, tenants[0], core.ID{}, &got) assert.Eq(t, err, core.ErrNotFound) }) t.Run("List with empty tenant", func(t *testing.T) { h := setup(t) var page core.Page[iam.TenantMember] err := h.Store.List(h.Context, core.ID{}, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListByUser with empty user", func(t *testing.T) { h := setup(t) var page core.Page[iam.TenantMember] err := h.Store.ListByUser(h.Context, core.ID{}, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) } func generateIDs(n int) []core.ID { ids := make([]core.ID, n) for i := range n { ids[i] = core.NewID("t") } slices.SortFunc(ids, func(a, b core.ID) int { return strings.Compare(a.String(), b.String()) }) return ids } func TestSqliteTenantMemberStore(t *testing.T) { testTenantMemberStore(t, func(t *testing.T) tenantMemberTestHarness { t.Helper() db := setupTestDB(t) tx, err := db.Begin() assert.Ok(t, err) t.Cleanup(func() { tx.Rollback() }) guard := lib.ContextAccessGuard{} ctx := lib.PutAccess(t.Context(), lib.AdminAccess()) w := dbi.WrapTx(tx) tenantStore := store.NewSqliteTenantStore(w, guard) userStore := store.NewSqliteUserStore(w, guard) return tenantMemberTestHarness{ Context: ctx, Store: store.NewSqliteTenantMemberStore(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: "Tenant " + id.String()}) 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", Verified: true}) assert.Ok(t, err) } return ids }, } }) }