package store_test import ( "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" ) func TestSqliteTenantInvitationStore(t *testing.T) { 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) 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 } newStore := func() *store.SqliteTenantInvitationStore { return store.NewSqliteTenantInvitationStore(w, guard) } t.Run("Create returns key.code token", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "alice@test.com") assert.Ok(t, err) key, code, ok := strings.Cut(token, ".") if !ok { t.Fatal("token missing dot separator") } if key == "" || code == "" { t.Fatalf("empty key or code: key=%q code=%q", key, code) } }) t.Run("Create lowercases email", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "Alice@Example.Com") assert.Ok(t, err) var inv iam.TenantInvitation assert.Ok(t, s.GetByToken(ctx, token, &inv)) assert.Eq(t, inv.Email, "alice@example.com") }) t.Run("Create duplicate returns ErrAlreadyExists", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] _, err := s.Create(ctx, tenant, "bob@test.com") assert.Ok(t, err) _, err = s.Create(ctx, tenant, "bob@test.com") assert.Eq(t, err, iam.ErrAlreadyExists) }) t.Run("Create on missing tenant returns ErrTenantNotFound", func(t *testing.T) { s := newStore() _, err := s.Create(ctx, core.NewID("t"), "ghost@test.com") assert.Eq(t, err, iam.ErrTenantNotFound) }) t.Run("GetByToken round-trip", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "carol@test.com") assert.Ok(t, err) var inv iam.TenantInvitation assert.Ok(t, s.GetByToken(ctx, token, &inv)) assert.Eq(t, inv.Tenant, tenant) assert.Eq(t, inv.Email, "carol@test.com") }) t.Run("GetByToken with no dot returns ErrNotFound", func(t *testing.T) { s := newStore() var inv iam.TenantInvitation err := s.GetByToken(ctx, "no-dot-here", &inv) assert.Eq(t, err, core.ErrNotFound) }) t.Run("GetByToken with unknown key returns ErrNotFound", func(t *testing.T) { s := newStore() var inv iam.TenantInvitation err := s.GetByToken(ctx, "unknownkey.unknowncode", &inv) assert.Eq(t, err, core.ErrNotFound) }) t.Run("GetByToken with wrong code returns ErrNotFound", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "dave@test.com") assert.Ok(t, err) key, _, _ := strings.Cut(token, ".") tampered := key + ".wrongcode" var inv iam.TenantInvitation err = s.GetByToken(ctx, tampered, &inv) assert.Eq(t, err, core.ErrNotFound) }) t.Run("GetByToken expired returns ErrNotFound", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "eve@test.com") assert.Ok(t, err) // Backdate the invitation past its expiry. key, _, _ := strings.Cut(token, ".") _, err = w.Exec(ctx, `UPDATE tenant_invitations SET expires_at = datetime('now', '-1 day') WHERE token_key = $1`, key) assert.Ok(t, err) var inv iam.TenantInvitation err = s.GetByToken(ctx, token, &inv) assert.Eq(t, err, core.ErrNotFound) }) t.Run("DeleteByToken removes row", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "frank@test.com") assert.Ok(t, err) assert.Ok(t, s.DeleteByToken(ctx, token)) var inv iam.TenantInvitation err = s.GetByToken(ctx, token, &inv) assert.Eq(t, err, core.ErrNotFound) }) t.Run("DeleteByToken with wrong code returns ErrNotFound and does not delete", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "grace@test.com") assert.Ok(t, err) key, _, _ := strings.Cut(token, ".") tampered := key + ".wrongcode" err = s.DeleteByToken(ctx, tampered) assert.Eq(t, err, core.ErrNotFound) // The original token still resolves. var inv iam.TenantInvitation assert.Ok(t, s.GetByToken(ctx, token, &inv)) }) t.Run("List by tenant", func(t *testing.T) { s := newStore() tenants := newTenants(t, 2) _, err := s.Create(ctx, tenants[0], "h1@test.com") assert.Ok(t, err) _, err = s.Create(ctx, tenants[0], "h2@test.com") assert.Ok(t, err) _, err = s.Create(ctx, tenants[1], "h3@test.com") assert.Ok(t, err) var page core.Page[iam.TenantInvitation] assert.Ok(t, s.List(ctx, tenants[0], core.PageReq{}, &page)) assert.Eq(t, len(page.Items), 2) }) t.Run("ListByEmail as system", func(t *testing.T) { s := newStore() tenants := newTenants(t, 2) _, err := s.Create(ctx, tenants[0], "multi@test.com") assert.Ok(t, err) _, err = s.Create(ctx, tenants[1], "multi@test.com") assert.Ok(t, err) var page core.Page[iam.TenantInvitation] assert.Ok(t, s.ListByEmail(ctx, "multi@test.com", core.PageReq{}, &page)) assert.Eq(t, len(page.Items), 2) }) t.Run("Delete by tenant+email", func(t *testing.T) { s := newStore() tenant := newTenants(t, 1)[0] token, err := s.Create(ctx, tenant, "rescind@test.com") assert.Ok(t, err) assert.Ok(t, s.Delete(ctx, tenant, "rescind@test.com")) var inv iam.TenantInvitation err = s.GetByToken(ctx, token, &inv) assert.Eq(t, err, core.ErrNotFound) }) }