// Unit tests with `@voltro/testing` — no database, no running server. // // 1. `authors.withBooks` — the runnable primitive. The executor returns a // `database.authors.with({ books: … })` query BUILDER; we run its // `.descriptor` through `ctx.store.query`, which (a) resolves the eager // `books` relation against the in-memory store and (b) AND-merges the // caller's tenant scope — the exact isolation the runtime applies. So we // assert both eager-loading AND tenant isolation. // 2. `books.search` — descriptor-pinned. Its executor calls `.matching(...)` // (dialect FTS: postgres tsvector / mysql MATCH…AGAINST / sqlite FTS5). // The in-memory test store does NOT evaluate `descriptor.fullTextSearch`, // so a run there would silently return UNFILTERED rows — a misleading // assertion. FTS behavior is proven by an integration test against a real // dialect; here we pin the wire contract (name / source / input+output // decode) instead of faking a search. // // Run with `voltro test` (vitest). import { describe, it, expect, beforeAll } from 'vitest' import { Schema } from 'effect' import { registerCoreTables, registerRelations } from '@voltro/database' import { makeTestContext, mockStore } from '@voltro/testing' import { database, actors, tenants } from '../database/index' // registers authors / books (tables) import { authorsRelations } from '../database/authors.relations' import { booksRelations } from '../database/books.relations' import authorsWithBooksExecute from '../queries/authors.withBooks.query.server' import { searchBooks } from '../queries/books.search.query' // Two boot steps `voltro dev` runs that the unit harness (`voltro test`) does // NOT, both needed for the eager-load walker: // 1. registerCoreTables — the audit()/tenant() mixins add createdBy/updatedBy/ // tenantId reference() columns whose thunks resolve `actors`/`tenants` via // this registry. `inferForeignKey` walks EVERY reference() column on `books` // to find the FK back to `authors`, so it dereferences those mixin columns — // which throw unless the core tables are registered. // 2. registerRelations — `*.relations.ts` files only DEFINE specs; runtime // module discovery (registerDiscoveredRelations) is what REGISTERS them. // We do both here — the exact wiring the dev/serve dispatcher does at boot — so // `.with({ books })` resolves against the in-memory store just as at runtime. beforeAll(() => { registerCoreTables({ actors: () => actors, tenants: () => tenants }) registerRelations(authorsRelations) registerRelations(booksRelations) }) // `bio` is an `.encrypted()` column. We seed it `null` (the descriptor allows // NullOr) — a plaintext/null value passes the read cipher through untouched, so // no governance cipher needs wiring for these reads. const book = (over: Partial> & { id: string; authorId: string; tenantId: string }) => ({ title: 'Untitled', summary: '', genre: 'fiction', tags: [] as ReadonlyArray, slug: 'untitled', ...over, }) describe('authors.withBooks', () => { it('returns the tenant’s authors with their books eager-loaded', async () => { const ctx = makeTestContext({ subject: { type: 'user', id: 'user_1', tenantId: 'acme' }, store: mockStore({ authors: [{ id: 'author_1', name: 'Ada', bio: null, tenantId: 'acme' }], books: [ book({ id: 'book_1', authorId: 'author_1', tenantId: 'acme', title: 'Analytical Engine' }), book({ id: 'book_2', authorId: 'author_1', tenantId: 'acme', title: 'Notes' }), ], }), }) const rows = await ctx.store.query(authorsWithBooksExecute({}, ctx).descriptor) expect(rows).toHaveLength(1) // Narrow BEFORE describing the shape: under `noUncheckedIndexedAccess` // `rows[0]` is `Row | undefined`, and a cast that spans that gap is the one // TypeScript refuses outright ("neither type sufficiently overlaps"). const first = rows[0] if (first === undefined) throw new Error('expected one author row') const ada = first as unknown as { id: string; name: string; books: ReadonlyArray<{ title: string }> } expect(ada.name).toBe('Ada') expect(ada.books.map((b) => b.title).sort()).toEqual(['Analytical Engine', 'Notes']) }) it('isolates tenants — t2 never sees t1 authors (REAL tenant scoping)', async () => { const ctx = makeTestContext({ subject: { type: 'user', id: 'u1', tenantId: 't1' }, store: mockStore({ authors: [{ id: 'author_t1', name: 'Secret', bio: null, tenantId: 't1' }], books: [book({ id: 'book_t1', authorId: 'author_t1', tenantId: 't1' })], }), }) // Same store, re-scoped to t2 — the runtime AND-merges eq('tenantId','t2'), // so t1's author is invisible. const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(authorsWithBooksExecute({}, c).descriptor), ) expect(seenByT2).toHaveLength(0) }) }) describe('books.search (query descriptor)', () => { it('declares the expected wire name + source table', () => { expect(searchBooks.name).toBe('books.search') expect(searchBooks.source).toBe('books') }) it('accepts a valid { q } input and rejects a malformed one', () => { const decode = Schema.decodeUnknownSync(searchBooks.input) expect(decode({ q: 'effect' })).toEqual({ q: 'effect' }) expect(() => decode({})).toThrow() // missing q }) it('decodes a well-formed book row against the output schema', () => { const decode = Schema.decodeUnknownSync(searchBooks.output) const row = decode({ id: 'book_1', authorId: 'author_1', title: 'Effect in Action', summary: 'A book.', genre: 'nonfiction', tags: ['effect', 'ts'], slug: 'effect-in-action', tenantId: 'acme', }) expect(row.tags).toEqual(['effect', 'ts']) expect(() => decode({ id: 'book_1' })).toThrow() // missing required fields }) })