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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
385 lines · 10.0 KB Raw History
// @pdsjs/core/loader tests - Repository loader from CAR files
import { beforeEach, describe, expect, it } from 'vitest';
import {
  getCarRepoInfo,
  loadRepositoryFromCar,
  validateCarFile,
} from '../packages/core/src/loader.js';
import { buildMst } from '../packages/core/src/mst.js';
import {
  buildCarFile,
  CID,
  cborEncodeDagCbor,
  cidToBytes,
  cidToString,
  createCid,
  createTid,
} from '../packages/core/src/repo.js';

// In-memory storage for tests
function createMockStorage() {
  /** @type {Map<string, Uint8Array>} */
  const blocks = new Map();
  /** @type {Map<string, {cid: string, value: Uint8Array}>} */
  const records = new Map();
  /** @type {Array<{seq: number, cid: string, rev: string, prev: string|null}>} */
  const commits = [];
  /** @type {{did: string|null, handle: string|null, privateKey: Uint8Array|null, kemKey: Uint8Array|null}} */
  const metadata = { did: null, handle: null, privateKey: null, kemKey: null };

  return {
    // Block operations
    async getBlock(/** @type {string} */ cid) {
      return blocks.get(cid) || null;
    },
    async putBlock(/** @type {string} */ cid, /** @type {Uint8Array} */ data) {
      blocks.set(cid, data);
    },

    // Record operations
    async getRecord(/** @type {string} */ uri) {
      const rec = records.get(uri);
      return rec || null;
    },
    async putRecord(
      /** @type {string} */ uri,
      /** @type {string} */ cid,
      /** @type {string} */ _collection,
      /** @type {string} */ _rkey,
      /** @type {any} */ value,
    ) {
      records.set(uri, { cid, value });
    },
    async listRecords(
      /** @type {string} */ collection,
      /** @type {any} */ _cursor,
      /** @type {number} */ limit,
    ) {
      /** @type {any[]} */
      const all = [];
      for (const [uri, rec] of records) {
        if (uri.includes(`/${collection}/`)) {
          const parts = uri.split('/');
          all.push({
            uri,
            cid: rec.cid,
            value: rec.value,
            rkey: parts[parts.length - 1],
          });
        }
      }
      return { records: all.slice(0, limit), cursor: null };
    },
    async listAllRecords() {
      const all = [];
      for (const [uri, rec] of records) {
        const parts = uri.replace('at://', '').split('/');
        const key = `${parts[1]}/${parts[2]}`;
        all.push({ key, cid: rec.cid });
      }
      return all;
    },
    async deleteRecord(/** @type {string} */ uri) {
      records.delete(uri);
    },

    // Commit operations
    async getLatestCommit() {
      return commits.length > 0 ? commits[commits.length - 1] : null;
    },
    async putCommit(
      /** @type {number} */ seq,
      /** @type {string} */ cid,
      /** @type {string} */ rev,
      /** @type {string | null} */ prev,
    ) {
      commits.push({ seq, cid, rev, prev });
    },

    // Metadata operations
    async getDid() {
      return metadata.did;
    },
    async setDid(/** @type {string} */ did) {
      metadata.did = did;
    },
    async getHandle() {
      return metadata.handle;
    },
    async setHandle(/** @type {string} */ handle) {
      metadata.handle = handle;
    },
    async getPrivateKey() {
      return metadata.privateKey;
    },
    async setPrivateKey(/** @type {Uint8Array} */ key) {
      metadata.privateKey = key;
    },
    async getKemKey() {
      return metadata.kemKey ?? null;
    },
    async setKemKey(/** @type {Uint8Array} */ key) {
      metadata.kemKey = key;
    },
    async getPreferences() {
      return [];
    },
    async setPreferences() {},

    // Events
    async getEvents() {
      return { events: [], cursor: 0 };
    },
    async putEvent() {},

    // Blobs
    async getBlob() {
      return null;
    },
    async putBlob() {},
    async deleteBlob() {},
    async listBlobs() {
      return { cids: [], cursor: null };
    },
    async getOrphanedBlobs() {
      return [];
    },
    async linkBlobToRecord() {},
    async unlinkBlobsFromRecord() {},

    // Test helpers
    _getBlockCount() {
      return blocks.size;
    },
    _getRecordCount() {
      return records.size;
    },
    _getCommits() {
      return commits;
    },
  };
}

/**
 * Build a test CAR file with records
 * @param {string} did
 * @param {Array<{collection: string, rkey: string, record: object}>} recordsToInclude
 * @returns {Promise<Uint8Array>}
 */
async function buildTestCar(did, recordsToInclude) {
  /** @type {Array<{cid: string, data: Uint8Array}>} */
  const blocks = [];
  /** @type {Map<string, Uint8Array>} */
  const blockMap = new Map();

  // Encode records and build MST entries
  /** @type {Array<{key: string, cid: string}>} */
  const mstEntries = [];

  for (const { collection, rkey, record } of recordsToInclude) {
    const recordBytes = cborEncodeDagCbor(record);
    const recordCid = cidToString(await createCid(recordBytes));
    blocks.push({ cid: recordCid, data: recordBytes });
    blockMap.set(recordCid, recordBytes);
    mstEntries.push({ key: `${collection}/${rkey}`, cid: recordCid });
  }

  // Sort entries for MST
  mstEntries.sort((a, b) => a.key.localeCompare(b.key));

  // Build MST
  const mstRoot = await buildMst(mstEntries, async (cid, data) => {
    blocks.push({ cid, data });
    blockMap.set(cid, data);
  });

  // Build commit
  const rev = createTid();
  const commit = {
    did,
    version: 3,
    rev,
    prev: null,
    data: mstRoot ? new CID(cidToBytes(mstRoot)) : null,
  };

  // Add signature field (empty for test)
  const signedCommit = { ...commit, sig: new Uint8Array(64) };
  const commitBytes = cborEncodeDagCbor(signedCommit);
  const commitCid = cidToString(await createCid(commitBytes));
  blocks.push({ cid: commitCid, data: commitBytes });

  return buildCarFile(commitCid, blocks);
}

describe('loadRepositoryFromCar', () => {
  /** @type {ReturnType<typeof createMockStorage>} */
  let storage;

  beforeEach(() => {
    storage = createMockStorage();
  });

  it('should load empty repository', async () => {
    const did = 'did:plc:test123';
    const car = await buildTestCar(did, []);

    const result = await loadRepositoryFromCar(
      car,
      /** @type {any} */ (storage),
    );

    expect(result.did).toBe(did);
    expect(result.recordCount).toBe(0);
    expect(result.blockCount).toBeGreaterThan(0);
    expect(await storage.getDid()).toBe(did);
  });

  it('should load repository with single record', async () => {
    const did = 'did:plc:test123';
    const car = await buildTestCar(did, [
      {
        collection: 'app.bsky.feed.post',
        rkey: '3abc123',
        record: {
          $type: 'app.bsky.feed.post',
          text: 'Hello World',
          createdAt: '2024-01-01T00:00:00Z',
        },
      },
    ]);

    const result = await loadRepositoryFromCar(
      car,
      /** @type {any} */ (storage),
    );

    expect(result.did).toBe(did);
    expect(result.recordCount).toBe(1);

    const record = await storage.getRecord(
      `at://${did}/app.bsky.feed.post/3abc123`,
    );
    expect(record).not.toBeNull();
    expect(record?.cid).toBeDefined();
  });

  it('should load repository with multiple records', async () => {
    const did = 'did:plc:test456';
    const car = await buildTestCar(did, [
      {
        collection: 'app.bsky.feed.post',
        rkey: '3post1',
        record: {
          $type: 'app.bsky.feed.post',
          text: 'Post 1',
          createdAt: '2024-01-01T00:00:00Z',
        },
      },
      {
        collection: 'app.bsky.feed.post',
        rkey: '3post2',
        record: {
          $type: 'app.bsky.feed.post',
          text: 'Post 2',
          createdAt: '2024-01-02T00:00:00Z',
        },
      },
      {
        collection: 'app.bsky.actor.profile',
        rkey: 'self',
        record: { $type: 'app.bsky.actor.profile', displayName: 'Test User' },
      },
    ]);

    const result = await loadRepositoryFromCar(
      car,
      /** @type {any} */ (storage),
    );

    expect(result.did).toBe(did);
    expect(result.recordCount).toBe(3);

    // Verify all records exist
    expect(
      await storage.getRecord(`at://${did}/app.bsky.feed.post/3post1`),
    ).not.toBeNull();
    expect(
      await storage.getRecord(`at://${did}/app.bsky.feed.post/3post2`),
    ).not.toBeNull();
    expect(
      await storage.getRecord(`at://${did}/app.bsky.actor.profile/self`),
    ).not.toBeNull();
  });

  it('should create commit in storage', async () => {
    const did = 'did:plc:test789';
    const car = await buildTestCar(did, [
      {
        collection: 'app.bsky.feed.post',
        rkey: '3test',
        record: {
          $type: 'app.bsky.feed.post',
          text: 'Test',
          createdAt: '2024-01-01T00:00:00Z',
        },
      },
    ]);

    const result = await loadRepositoryFromCar(
      car,
      /** @type {any} */ (storage),
    );
    const commit = await storage.getLatestCommit();

    expect(commit).not.toBeNull();
    expect(commit?.cid).toBe(result.commitCid);
    expect(commit?.rev).toBe(result.rev);
    expect(commit?.seq).toBe(1);
  });
});

describe('getCarRepoInfo', () => {
  it('should extract DID and commit info', async () => {
    const did = 'did:plc:infotest';
    const car = await buildTestCar(did, [
      {
        collection: 'app.bsky.feed.post',
        rkey: '3test',
        record: {
          $type: 'app.bsky.feed.post',
          text: 'Test',
          createdAt: '2024-01-01T00:00:00Z',
        },
      },
    ]);

    const info = getCarRepoInfo(car);

    expect(info.did).toBe(did);
    expect(info.commitCid).toBeDefined();
    expect(info.rev).toBeDefined();
  });
});

describe('validateCarFile', () => {
  it('should validate correct CAR file', async () => {
    const did = 'did:plc:validtest';
    const car = await buildTestCar(did, []);

    const result = validateCarFile(car);

    expect(result.valid).toBe(true);
    expect(result.did).toBe(did);
    expect(result.error).toBeUndefined();
  });

  it('should reject malformed CAR', () => {
    const garbage = new Uint8Array([0xff, 0xff, 0xff]);

    const result = validateCarFile(garbage);

    expect(result.valid).toBe(false);
    expect(result.error).toBeDefined();
  });
});