// Unit test for `posts.create` — driven through `@voltro/testing`. // // Content moderation is enforced by the plugin's rpc INTERCEPTOR (a block rule // wired in app.config.ts), which runs BEFORE this executor and rejects banned // content with a typed `ContentRejected` — a dispatch-time middleware, NOT the // executor. `invoke` reproduces only the input-Schema decode, not middleware // (see its header), so we DON'T fake the interceptor: we test the executor's own // observable behavior (the tenant-stamped insert) + pin the descriptor's // declared `ContentRejected` error shape. Run with `voltro test` (vitest). import { describe, it, expect } from 'vitest' import { Schema } from 'effect' import { makeTestContext, mockStore, invoke } from '@voltro/testing' import { database } from '../database/schema' // registers actors / tenants / posts / comments import { createPost } from '../mutations/posts.create.mutation' import createPostHandler from '../mutations/posts.create.mutation.server' import { ContentRejected } from '@voltro/plugin-moderation/errors' const subject = { type: 'user', id: 'u1', tenantId: 'acme' } as const describe('posts.create', () => { it('inserts a post stamped with the caller tenant + a post_ TypeID', async () => { const ctx = makeTestContext({ subject, store: mockStore({ posts: [] }) }) const row = (await invoke( createPost, createPostHandler, { title: 'Hello', body: 'World' }, ctx, )) as { id: string; title: string; body: string; tenantId: string } expect(row.title).toBe('Hello') expect(row.body).toBe('World') expect(row.tenantId).toBe('acme') // tenant() stamped it from the subject expect(row.id).toMatch(/^post_/) // The row is really in the store — the live tenant-scoped query sees it. const rows = await ctx.store.query(database.posts.descriptor) expect(rows).toHaveLength(1) }) it('rejects an empty title at the input-schema decode (NonEmptyString)', async () => { const ctx = makeTestContext({ subject, store: mockStore({ posts: [] }) }) await expect( invoke(createPost, createPostHandler, { title: '', body: '' }, ctx), ).rejects.toThrow() }) it('isolates tenants — a second tenant never sees the first tenant post', async () => { const ctx = makeTestContext({ subject: { type: 'user', id: 'u1', tenantId: 't1' }, store: mockStore({ posts: [] }), }) await invoke(createPost, createPostHandler, { title: 'secret', body: '' }, ctx) const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.posts.descriptor)) expect(seenByT2).toHaveLength(0) }) it('pins the descriptor: name, input decode + the declared ContentRejected error', async () => { // The moderation error is a dispatch-time interceptor concern, not reachable // from the executor here — so we pin the CONTRACT the client relies on: the // rpc tag, that the input Schema decodes a valid post, and that the typed // ContentRejected error the interceptor raises is declared on the procedure. expect(createPost.name).toBe('posts.create') const decoded = Schema.decodeUnknownSync(createPost.input)({ title: 'ok', body: 'b' }) expect(decoded).toEqual({ title: 'ok', body: 'b' }) const rejected = new ContentRejected({ tag: 'posts.create', categories: ['profanity'], reason: 'banned term', }) expect(rejected).toBeInstanceOf(ContentRejected) expect(Schema.is(ContentRejected)(rejected)).toBe(true) }) })