Last Updated: 2026-05-06 Status: Active
Test inventory, how to run tests, patterns, and coverage gaps.
1. Test Counts at a Glance
| Package | Tests | Runner | Last known pass |
|---|---|---|---|
apps/blog-service |
94 / 94 | Vitest | ✅ |
packages/blog-sdk |
394 / 394 | Vitest | ✅ |
apps/seller-dashboard (blog module) |
— | — | No unit tests |
2. Running Tests
# Run all tests across the monorepo
turbo run test
# Run just blog-service tests
cd apps/blog-service && npm run test
# Run just SDK tests
cd packages/blog-sdk && pnpm test
# Watch mode (blog-service)
cd apps/blog-service && npx vitest3. blog-service Tests
Location: apps/blog-service/test/ and apps/blog-service/src/ (co-located schemas)
Database: Tests use PGlite — an in-process Postgres that runs inside Node.js. Drizzle migrations are applied to a fresh PGlite instance before each test suite. No external database needed.
Test Files
| File | What it tests |
|---|---|
test/smoke.test.ts |
Boot test — PGlite + migrations + basic insert/read. First test to run; if this fails, something is broken at the DB layer. |
test/tenant-isolation.test.ts |
Multi-tenancy security — Verifies that tenant A cannot read, modify, or delete tenant B's posts, categories, or tags. Tests the core IDOR (Insecure Direct Object Reference) attack surface. |
test/slug-collision.test.ts |
Slug uniqueness — duplicate title in same tenant gets -1, -2, etc. suffix. Cross-tenant duplicates are allowed. |
test/publish-flow.test.ts |
Status transitions — draft → published → unpublished → scheduled. Verifies published_at is set/cleared correctly. |
test/tiptap-renderer.test.ts |
HTML renderer — Tests every supported TipTap node type renders to the expected HTML. Covers security: javascript: URLs blocked, data: URLs blocked, HTML-escaping of special chars. |
src/schemas/post.schema.test.ts |
Zod schema validation — valid and invalid inputs for createPostSchema, updatePostSchema, listPostsSchema. |
src/services/post.utils.test.ts |
Utility functions — slug generation from various titles, slug uniqueness logic, ID format. |
Test Setup
// test/setup.ts (pattern)
import { PGlite } from "@electric-sql/pglite"
import { drizzle } from "drizzle-orm/pglite"
import { migrate } from "drizzle-orm/pglite/migrator"
export async function createTestDb() {
const pglite = new PGlite()
const db = drizzle(pglite, { schema })
await migrate(db, { migrationsFolder: "./drizzle" })
return db
}Tests use makeTestApp(db, context) — a factory that creates the full Hono app wired to a test DB and a fake request context (tenant_id, user_id, permissions). This means route handler tests go through the actual middleware pipeline.
Key Test Patterns
Testing tenant isolation (most important pattern):
// Seed data for two tenants
await seedPost(db, { tenantId: TENANT_A, title: "A-post-1" })
await seedPost(db, { tenantId: TENANT_B, title: "B-post-1" })
// App running as TENANT_A
const app = makeTestApp(db, { tenantId: TENANT_A })
// TENANT_A can read their own post
const res = await app.request("/admin/posts/a-post-1-id")
expect(res.status).toBe(200)
// TENANT_A cannot read TENANT_B's post — returns 404, not 403
// (404 is correct: leaking "exists but forbidden" is an information disclosure)
const res2 = await app.request("/admin/posts/b-post-1-id")
expect(res2.status).toBe(404)Testing the renderer (typical pattern):
// test/tiptap-renderer.test.ts
it("renders a table with header and data rows", () => {
const result = renderTipTap({
type: "doc",
content: [{
type: "table",
content: [/* ... */]
}]
})
expect(result).toContain("<table>")
expect(result).toContain("<thead>")
expect(result).toContain("<tbody>")
})
it("blocks javascript: URLs in links", () => {
const result = renderTipTap({
type: "doc",
content: [{ type: "paragraph", content: [{
type: "text",
text: "click me",
marks: [{ type: "link", attrs: { href: "javascript:alert(1)" }}]
}]}]
})
expect(result).toContain('href="#"') // blocked, replaced with #
expect(result).not.toContain("javascript:")
})4. @vlozi/blog SDK Tests
Location: packages/blog-sdk/test/
Approach: Pure unit tests — no real HTTP calls, no server needed. Uses vi.fn() to mock fetch.
Test Files
| File | What it tests |
|---|---|
client.test.ts |
VloziClient — list, get, categories, tags; query parameter construction; error handling; all VloziError subclasses |
sanitize.test.ts |
sanitizeHtml() — 5-pass sanitizer; XSS vectors (script tags, onerror, javascript: hrefs, data: URLs); allowed vs disallowed tags |
hydrate.test.ts |
Client-side hydration scanner — Mermaid, YouTube, Carousel node detection and mount |
cache.test.ts |
InMemoryCache — TTL expiry, SWR (stale-while-revalidate) behavior, max entry limit |
cache-integration.test.ts |
Cache + client integration — hooks serve from cache on second call, revalidate in background |
feeds.test.ts |
RSS/Atom feed generation — correct XML structure, escaped entities, required fields |
next.test.ts |
generateStaticParamsForPosts, generateMetadataForPost — correct output shapes for Next.js |
api-surface.test.ts |
Smoke test of all public exports — nothing is accidentally removed from index.ts |
build-artifacts.test.ts |
Verifies the compiled package has all four entry points and CSS file |
resilience.test.ts |
Network failures, 429 rate limiting, malformed JSON responses — error propagation |
Key Test Patterns
Mocking fetch:
import { vi, expect } from "vitest"
import { VloziClient } from "../src/client"
const mockFetch = vi.fn()
global.fetch = mockFetch
beforeEach(() => {
mockFetch.mockReset()
})
it("returns paginated posts", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: [{ title: "Post 1", slug: "post-1" }], meta: { total: 1, page: 1, limit: 10, totalPages: 1 } }),
})
const client = new VloziClient({ apiKey: "test-key", baseUrl: "https://example.com" })
const result = await client.blog.list()
expect(result.data).toHaveLength(1)
expect(mockFetch).toHaveBeenCalledWith(
"https://example.com/blog/public/posts?page=1&limit=10",
expect.objectContaining({ headers: expect.any(Headers) })
)
})Testing sanitization:
it("strips script tags", () => {
const dirty = '<p>hello</p><script>alert(1)</script>'
expect(sanitizeHtml(dirty)).not.toContain("<script>")
expect(sanitizeHtml(dirty)).toContain("hello")
})
it("blocks javascript: hrefs", () => {
const dirty = '<a href="javascript:void(0)">click</a>'
const clean = sanitizeHtml(dirty)
expect(clean).not.toContain("javascript:")
})5. Coverage Gaps
| Area | Current coverage | Risk |
|---|---|---|
| blog-service route handlers (integration) | ✅ Covered via makeTestApp |
Low |
| blog-service tenant isolation | ✅ Covered | Low |
| TipTap renderer — all node types | ✅ Covered | Low |
| Zod validation schemas | ✅ Covered | Low |
| SDK client — all methods | ✅ Covered | Low |
| SDK sanitizer — XSS vectors | ✅ Covered | Low |
| SDK cache behavior | ✅ Covered | Low |
| Dashboard editor (React) | ❌ No unit/integration tests | Medium — untested autosave, dirty state, slash commands |
| Gateway blog proxy | ❌ No tests | Medium — integration tested manually |
| Markdown import pipeline | ❌ No unit tests | Medium — parse-import.ts, markdown-to-tiptap.ts are complex |
| Carousel renderer | N/A — not implemented | Known bug |
| Scheduled publishing cron | N/A — not implemented | — |
6. Adding Tests
blog-service: New Route Test
- Add a test file in
apps/blog-service/test/ - Import
createTestDbandmakeTestAppfrom./setup - Use
seedPost(),seedCategory(),seedTag()from./fixturesto set up data - Make requests via
app.request(path, options)— this is the full Hono app, no HTTP server needed - Assert on
res.statusandawait res.json()
SDK: New Unit Test
- Add a test file in
packages/blog-sdk/test/ - Mock
fetchat the module level withvi.fn() - Import the function/class under test from
../src/ - Reset mocks in
beforeEach
Running a Single Test File
# blog-service
cd apps/blog-service && npx vitest run test/slug-collision.test.ts
# SDK
cd packages/blog-sdk && npx vitest run test/sanitize.test.ts