Test an application workflow across services

Use private file access to test authentication, persistence, validation and cleanup together.

On this pageSource-backed Markdown

Use 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.

Copy the shipped example
bash
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.

Run the lab
bash
npx tsx examples/runPrivateFiles.ts

Assert 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.

examples/runPrivateFiles.ts
ts
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.

tests/media/runPrivateFiles.test.ts
ts
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.

Run your copied test and check types
bash
npx vitest run tests/media/runPrivateFiles.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts

Know 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.

Behaviour tested Executed by the documentation maintenance gate
What this does

Exercises an application-owned HTTP file workflow using real Auth, ActiveRecord, Media and local Storage.

Expected outputThe guide test passes against the real framework components.
Behaviour testpackages/app/src/media/tests/examples/runPrivateFiles.test.ts

This test command requires the framework repository. Use the walkthrough commands in an installed application.

Environment: Disposable SQL, temporary local files and Fastify injection.