forked from
chadtmiller.com/pds.js
A minimal AT Protocol Personal Data Server written in JavaScript.
1// test/helpers/node-server.js
2import { mkdirSync, rmSync } from 'node:fs';
3import { createServer } from '@pds/node';
4import { LexiconResolver } from '@pds/lexicon-resolver';
5import { defineLexicon } from '@bigmoves/lexicon';
6
7const TEST_DATA_DIR = './test-data';
8
9// Minimal app.bsky.feed.post schema for e2e validation testing
10const postSchema = defineLexicon({
11 lexicon: 1,
12 id: 'app.bsky.feed.post',
13 defs: {
14 main: {
15 type: 'record',
16 key: 'tid',
17 record: {
18 type: 'object',
19 required: ['text', 'createdAt'],
20 properties: {
21 text: { type: 'string', maxLength: 3000 },
22 createdAt: { type: 'string', format: 'datetime' },
23 },
24 },
25 },
26 },
27});
28const TEST_PORT = 3000;
29const USE_LOCAL_INFRA = process.env.USE_LOCAL_INFRA !== 'false';
30const USE_S3 = process.env.BLOB_STORAGE === 's3';
31
32const MINIO_BUCKET = 'pds-blobs';
33
34/**
35 * Create blob adapter using MinIO client
36 * @returns {Promise<import('@pds/core/ports').BlobPort>}
37 */
38async function createMinioBlobs() {
39 const { Client } = await import('minio');
40
41 const client = new Client({
42 endPoint: 'localhost',
43 port: 9000,
44 useSSL: false,
45 accessKey: 'minioadmin',
46 secretKey: 'minioadmin',
47 });
48
49 // Ensure bucket exists
50 const exists = await client.bucketExists(MINIO_BUCKET);
51 if (!exists) {
52 await client.makeBucket(MINIO_BUCKET);
53 console.log('Created MinIO bucket:', MINIO_BUCKET);
54 }
55
56 return {
57 async get(did, cid) {
58 try {
59 const objectName = `${did}/${cid}`;
60 const stat = await client.statObject(MINIO_BUCKET, objectName);
61 const stream = await client.getObject(MINIO_BUCKET, objectName);
62
63 // Collect stream into buffer
64 const chunks = [];
65 for await (const chunk of stream) {
66 chunks.push(chunk);
67 }
68 const data = new Uint8Array(Buffer.concat(chunks));
69 const mimeType = stat.metaData?.mime_type || 'application/octet-stream';
70
71 return { data, mimeType };
72 } catch (e) {
73 if (e.code === 'NotFound') return null;
74 throw e;
75 }
76 },
77
78 async put(did, cid, data, mimeType) {
79 const objectName = `${did}/${cid}`;
80 await client.putObject(
81 MINIO_BUCKET,
82 objectName,
83 Buffer.from(data),
84 data.length,
85 {
86 'Content-Type': mimeType,
87 mime_type: mimeType,
88 },
89 );
90 },
91
92 async delete(did, cid) {
93 const objectName = `${did}/${cid}`;
94 await client.removeObject(MINIO_BUCKET, objectName);
95 },
96 };
97}
98
99/**
100 * Start Node.js PDS server for e2e tests
101 * Cleans test data directory for fresh state each run
102 * @param {Object} [options]
103 * @param {import('@pds/core/ports').LexiconResolverPort} [options.lexiconResolver] - Lexicon resolver for record validation
104 * @returns {Promise<{close: () => Promise<void>}>} Server instance with close() method
105 */
106export async function startNodeServer(options = {}) {
107 // Fresh data each run
108 rmSync(TEST_DATA_DIR, { recursive: true, force: true });
109 mkdirSync(TEST_DATA_DIR, { recursive: true });
110
111 // Configure blobs adapter
112 const blobs = USE_S3 ? await createMinioBlobs() : undefined;
113 if (USE_S3) {
114 console.log('Using S3 blob storage (MinIO)');
115 }
116
117 // Create default lexicon resolver with bsky post schema for validation testing
118 const lexiconResolver =
119 options.lexiconResolver ?? new LexiconResolver({ schemas: [postSchema] });
120
121 const server = await createServer({
122 port: TEST_PORT,
123 dbPath: `${TEST_DATA_DIR}/pds.db`,
124 blobsDir: `${TEST_DATA_DIR}/blobs`,
125 blobs,
126 jwtSecret: 'test-secret-for-e2e',
127 // Use HTTPS hostname (via Caddy proxy) when docker infra is enabled
128 hostname: USE_LOCAL_INFRA
129 ? 'host.docker.internal:3443'
130 : `localhost:${TEST_PORT}`,
131 password: 'test-password',
132 // Use local relay when docker infrastructure is available
133 relayUrl: USE_LOCAL_INFRA ? 'http://localhost:2470' : undefined,
134 // Keep appview pointing to production (for proxy tests)
135 appviewUrl: 'https://api.bsky.app',
136 appviewDid: 'did:web:api.bsky.app',
137 lexiconResolver,
138 });
139
140 await server.listen();
141 return server;
142}
143
144/**
145 * Stop Node.js PDS server
146 * @param {{close: () => Promise<void>}} server - Server instance from startNodeServer
147 */
148export async function stopNodeServer(server) {
149 await server.close();
150}
151
152export { USE_LOCAL_INFRA, TEST_PORT, USE_S3 };