~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
153 lines · 4.4 KB Raw History
// 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 };