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 grantTestHarness struct { Context context.Context Store iam.GrantStore 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 testGrantStore(t *testing.T, setup func(t *testing.T) grantTestHarness) { t.Run("Add and List", 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) g := iam.Grant{Tenant: tenant, Type: "user", Principal: user.String(), Role: "viewer", Path: ""} assert.Ok(t, h.Store.Add(ctx, g)) var page core.Page[iam.Grant] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 1) assert.Eq(t, page.Items[0], g) }) t.Run("Add 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) g := iam.Grant{Tenant: tenant, Type: "user", Principal: user.String(), Role: "viewer", Path: ""} assert.Ok(t, h.Store.Add(ctx, g)) assert.Ok(t, h.Store.Add(ctx, g)) var page core.Page[iam.Grant] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 1) }) t.Run("Add user not in tenant", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] user := h.NewUsers(t, 1)[0] // user is NOT added as tenant member g := iam.Grant{Tenant: tenant, Type: "user", Principal: user.String(), Role: "viewer", Path: ""} err := h.Store.Add(ctx, g) assert.Eq(t, err != nil, true) }) t.Run("Add group type skips membership check", func(t *testing.T) { h := setup(t) ctx := h.Context tenant := h.NewTenants(t, 1)[0] g := iam.Grant{Tenant: tenant, Type: "group", Principal: "engineers", Role: "viewer", Path: ""} assert.Ok(t, h.Store.Add(ctx, g)) var page core.Page[iam.Grant] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 1) }) 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] h.AddMember(t, tenant, user) g := iam.Grant{Tenant: tenant, Type: "user", Principal: user.String(), Role: "viewer", Path: ""} assert.Ok(t, h.Store.Add(ctx, g)) assert.Ok(t, h.Store.Remove(ctx, g)) var page core.Page[iam.Grant] err := h.Store.List(ctx, tenant, core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("ListForUser by principal", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) users := h.NewUsers(t, 2) h.AddMember(t, tenants[0], users[0]) h.AddMember(t, tenants[1], users[0]) assert.Ok(t, h.Store.Add(ctx, iam.Grant{Tenant: tenants[0], Type: "user", Principal: users[0].String(), Role: "viewer"})) assert.Ok(t, h.Store.Add(ctx, iam.Grant{Tenant: tenants[1], Type: "user", Principal: users[0].String(), Role: "editor"})) // user1 grant — should not appear assert.Ok(t, h.Store.Add(ctx, iam.Grant{Tenant: tenants[0], Type: "group", Principal: "other", Role: "viewer"})) var page core.Page[iam.Grant] err := h.Store.ListForUser(ctx, users[0], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 2) }) t.Run("List isolation", func(t *testing.T) { h := setup(t) ctx := h.Context tenants := h.NewTenants(t, 2) user := h.NewUsers(t, 1)[0] h.AddMember(t, tenants[0], user) assert.Ok(t, h.Store.Add(ctx, iam.Grant{Tenant: tenants[0], Type: "user", Principal: user.String(), Role: "viewer"})) var page core.Page[iam.Grant] err := h.Store.List(ctx, tenants[1], core.PageReq{}, &page) assert.Ok(t, err) assert.Eq(t, len(page.Items), 0) }) t.Run("Add: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] err := h.Store.Add(t.Context(), iam.Grant{Tenant: tenant, Type: "group", Principal: "x", Role: "viewer"}) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("Remove: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] err := h.Store.Remove(t.Context(), iam.Grant{Tenant: tenant}) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("List: no access", func(t *testing.T) { h := setup(t) var page core.Page[iam.Grant] err := h.Store.List(t.Context(), core.ID{}, core.PageReq{}, &page) assert.Eq(t, err, iam.ErrForbidden) }) t.Run("List by tenant: no access", func(t *testing.T) { h := setup(t) tenant := h.NewTenants(t, 1)[0] var page core.Page[iam.Grant] err := h.Store.List(t.Context(), tenant, core.PageReq{}, &page) assert.Eq(t, err, iam.ErrForbidden) }) } func TestSqliteGrantStore(t *testing.T) { testGrantStore(t, func(t *testing.T) grantTestHarness { 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 grantTestHarness{ Context: ctx, Store: store.NewSqliteGrantStore(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) }, } }) }