chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// test/helpers/node-server.js
import { mkdirSync, rmSync } from 'node:fs';
import { defineLexicon } from '@bigmoves/lexicon';
import { LexiconResolver } from '@pdsjs/lexicon-resolver';
import { createServer } from '@pdsjs/node';
import { TEST_PORT } from './test-port.js';
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 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<import('@pdsjs/core/ports').BlobPort>}
*/
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('@pdsjs/core/ports').LexiconResolverPort} [options.lexiconResolver] - Lexicon resolver for record validation
* @returns {Promise<{close: () => Promise<void>}>} 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,
plcUrl: USE_LOCAL_INFRA ? 'http://localhost:2582' : 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<void>}} server - Server instance from startNodeServer
*/
export async function stopNodeServer(server) {
await server.close();
}
export { TEST_PORT, USE_LOCAL_INFRA, USE_S3 };