// test/helpers/node-server.js import { mkdirSync, rmSync } from 'node:fs'; import { createServer } from '@pds/node'; import { LexiconResolver } from '@pds/lexicon-resolver'; import { defineLexicon } from '@bigmoves/lexicon'; const TEST_DATA_DIR = './test-data'; // Minimal app.bsky.feed.post schema for e2e validation testing const postSchema = defineLexicon({ lexicon: 1, id: 'app.bsky.feed.post', defs: { main: { type: 'record', key: 'tid', record: { type: 'object', required: ['text', 'createdAt'], properties: { text: { type: 'string', maxLength: 3000 }, createdAt: { type: 'string', format: 'datetime' }, }, }, }, }, }); const TEST_PORT = 3000; const USE_LOCAL_INFRA = process.env.USE_LOCAL_INFRA !== 'false'; const USE_S3 = process.env.BLOB_STORAGE === 's3'; const MINIO_BUCKET = 'pds-blobs'; /** * Create blob adapter using MinIO client * @returns {Promise} */ async function createMinioBlobs() { const { Client } = await import('minio'); const client = new Client({ endPoint: 'localhost', port: 9000, useSSL: false, accessKey: 'minioadmin', secretKey: 'minioadmin', }); // Ensure bucket exists const exists = await client.bucketExists(MINIO_BUCKET); if (!exists) { await client.makeBucket(MINIO_BUCKET); console.log('Created MinIO bucket:', MINIO_BUCKET); } return { async get(did, cid) { try { const objectName = `${did}/${cid}`; const stat = await client.statObject(MINIO_BUCKET, objectName); const stream = await client.getObject(MINIO_BUCKET, objectName); // Collect stream into buffer const chunks = []; for await (const chunk of stream) { chunks.push(chunk); } const data = new Uint8Array(Buffer.concat(chunks)); const mimeType = stat.metaData?.mime_type || 'application/octet-stream'; return { data, mimeType }; } catch (e) { if (e.code === 'NotFound') return null; throw e; } }, async put(did, cid, data, mimeType) { const objectName = `${did}/${cid}`; await client.putObject( MINIO_BUCKET, objectName, Buffer.from(data), data.length, { 'Content-Type': mimeType, mime_type: mimeType, }, ); }, async delete(did, cid) { const objectName = `${did}/${cid}`; await client.removeObject(MINIO_BUCKET, objectName); }, }; } /** * Start Node.js PDS server for e2e tests * Cleans test data directory for fresh state each run * @param {Object} [options] * @param {import('@pds/core/ports').LexiconResolverPort} [options.lexiconResolver] - Lexicon resolver for record validation * @returns {Promise<{close: () => Promise}>} Server instance with close() method */ export async function startNodeServer(options = {}) { // Fresh data each run rmSync(TEST_DATA_DIR, { recursive: true, force: true }); mkdirSync(TEST_DATA_DIR, { recursive: true }); // Configure blobs adapter const blobs = USE_S3 ? await createMinioBlobs() : undefined; if (USE_S3) { console.log('Using S3 blob storage (MinIO)'); } // Create default lexicon resolver with bsky post schema for validation testing const lexiconResolver = options.lexiconResolver ?? new LexiconResolver({ schemas: [postSchema] }); const server = await createServer({ port: TEST_PORT, dbPath: `${TEST_DATA_DIR}/pds.db`, blobsDir: `${TEST_DATA_DIR}/blobs`, blobs, jwtSecret: 'test-secret-for-e2e', // Use HTTPS hostname (via Caddy proxy) when docker infra is enabled hostname: USE_LOCAL_INFRA ? 'host.docker.internal:3443' : `localhost:${TEST_PORT}`, password: 'test-password', // Use local relay when docker infrastructure is available relayUrl: USE_LOCAL_INFRA ? 'http://localhost:2470' : undefined, // Keep appview pointing to production (for proxy tests) appviewUrl: 'https://api.bsky.app', appviewDid: 'did:web:api.bsky.app', lexiconResolver, }); await server.listen(); return server; } /** * Stop Node.js PDS server * @param {{close: () => Promise}} server - Server instance from startNodeServer */ export async function stopNodeServer(server) { await server.close(); } export { USE_LOCAL_INFRA, TEST_PORT, USE_S3 };