Test an application workflow across services
Use private file access to test authentication, persistence, validation and cleanup together.
On this page
Source-backed MarkdownUse the private-file application
Complete Installation, including the dedicated SQL test account, and install fastify@5. This test owns a small application factory; it does not launch Scout or require a running public server.
Copy the maintained workflow
Copy the shipped Media examples. They include the real server factory and runner used by this test. Reusing them keeps the guide and test on one source of truth.
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/media/examples/. examples/Observe the workflow once
The runner registers two real users, uploads and reads a file, rejects foreign access and invalid input, deletes the file and retries. Expect the status codes and cleanup flags listed in the private-file guide.
npx tsx examples/runPrivateFiles.tsAssert both the response and the effect
A foreign request must return 404 and must not delete the owner’s bytes. Rejected input must not create another Media row. A successful delete must remove the bytes and make a later read return 404.
Keep a valid retry after each deliberately broken state. A test that merely catches an exception can miss a poisoned connection, retained claim or leaked stream. The runner closes the HTTP server, App, generated database and temporary directory even after failure.
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { AuthProvider, AuthToken, PasswordResetToken, UserIdentity } from '@db3.ai/app/auth';
import { createGeneratedTestDatabase } from '@db3.ai/app/db/test/db';
import { MediaFile, MediaItem, MediaLibrary } from '@db3.ai/app/media';
import { App } from '@db3.ai/app/server';
import { createPrivateFileServer } from './createPrivateFileServer';
/** Runs authorized upload/download/deletion against real Auth, SQL and storage. */
export async function runPrivateFiles() {
const root = await mkdtemp(join(tmpdir(), 'db3-private-files-'));
try {
const database = await createGeneratedTestDatabase('private_files');
const application = new App({ db: database.db, config: { auth: { providers: { password: true } } }, storage: { disks: { local: { driver: 'local', root } } } });
const server = createPrivateFileServer(application);
try {
await application.db.install(UserIdentity, AuthProvider, AuthToken, PasswordResetToken, MediaLibrary, MediaFile, MediaItem);
const ada = await application.auth.registerWithPassword({ name: 'Ada', email: '[email protected]', password: 'example-password-123' });
const grace = await application.auth.registerWithPassword({ name: 'Grace', email: '[email protected]', password: 'example-password-456' });
const headers = { authorization: `Bearer ${ada.token}`, 'content-type': 'text/plain' };
const other = { authorization: `Bearer ${grace.token}` };
const unsigned = await server.inject({ method: 'POST', url: '/files', headers: { 'content-type': 'text/plain' }, payload: 'Hidden' });
const upload = await server.inject({ method: 'POST', url: '/files', headers, payload: 'Private client brief' });
if (upload.statusCode !== 201) throw new Error(`Upload failed: ${upload.statusCode}`);
const id = upload.json<{ id: string }>().id;
const file = await MediaFile.where('id', id).firstOrFail();
const path = file.path!;
const download = await server.inject({ url: `/files/${id}`, headers });
const stranger = await server.inject({ url: `/files/${id}`, headers: other });
const strangerDelete = await server.inject({ method: 'DELETE', url: `/files/${id}`, headers: other });
const empty = await server.inject({ method: 'POST', url: '/files', headers, payload: ' ' });
const wrongType = await server.inject({ method: 'POST', url: '/files', headers: { ...headers, 'content-type': 'application/json' }, payload: { owner: grace.user.id } });
const oversized = await server.inject({ method: 'POST', url: '/files', headers, payload: 'x'.repeat(64 * 1024 + 1) });
const failedWritesAbsent = await MediaFile.query().count() === 1;
const deleted = await server.inject({ method: 'DELETE', url: `/files/${id}`, headers });
const missing = await server.inject({ url: `/files/${id}`, headers });
const retry = await server.inject({ method: 'POST', url: '/files', headers, payload: 'Revised brief' });
return { uploaded: upload.statusCode, text: download.body, unsigned: unsigned.statusCode, stranger: stranger.statusCode, strangerDelete: strangerDelete.statusCode, empty: empty.statusCode, wrongType: wrongType.statusCode, oversized: oversized.statusCode, failedWritesAbsent, privateHeaders: download.headers['cache-control'] === 'private, no-store' && download.headers['x-content-type-options'] === 'nosniff', deleted: deleted.statusCode, bytesRemoved: await application.storage.missing(path), missing: missing.statusCode, retry: retry.statusCode };
} finally { try { await server.close(); } finally { try { await application.close(); } finally { await database.destroy(); } } }
} finally { await rm(root, { recursive: true, force: true }); }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) console.log(JSON.stringify(await runPrivateFiles(), null, 2));
Testing
Create a tests/media directory and save the test below as runPrivateFiles.test.ts. It imports the example you copied into examples/. Keep the same folder layout so the relative import resolves.
Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout. This database lab needs the same test-only SQL credentials when run through Vitest.
import { expect, it } from 'vitest';
import { runPrivateFiles } from '../../examples/runPrivateFiles';
it('protects real managed files across users and rejects invalid uploads without writes', async () => {
expect(await runPrivateFiles()).toEqual({ uploaded: 201, text: 'Private client brief', unsigned: 401, stranger: 404, strangerDelete: 404, empty: 400, wrongType: 400, oversized: 413, failedWritesAbsent: true, privateHeaders: true, deleted: 204, bytesRemoved: true, missing: 404, retry: 201 });
});
Run and change the scenario
Try another user, changed file content or a stricter size policy. Extend the assertion before changing the route, then run this test and compile the copied examples. No paid provider is involved.
npx vitest run tests/media/runPrivateFiles.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.tsKnow the remaining gaps
This scenario proves real service behavior through injected HTTP. It does not prove browser cookie handling, proxies, socket disconnects, a cloud storage provider or a deployed migration. Add separate checks for those boundaries when your feature crosses them.
Exercises an application-owned HTTP file workflow using real Auth, ActiveRecord, Media and local Storage.
The guide test passes against the real framework components.packages/app/src/media/tests/examples/runPrivateFiles.test.tsThis test command requires the framework repository. Use the walkthrough commands in an installed application.
Environment: Disposable SQL, temporary local files and Fastify injection.