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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
300 lines · 8.1 KB Raw History
// @pdsjs/readonly/build-car tests
import { describe, expect, it } from 'vitest';
import { parseCarFile } from '../packages/core/src/car.js';
import { generateKeyPair } from '../packages/core/src/crypto.js';
import { loadRepositoryFromCar } from '../packages/core/src/loader.js';
import { cborDecode } from '../packages/core/src/repo.js';
import { buildRepositoryCar } from '../packages/readonly/src/build-car.js';

// Mock storage for loading the CAR
function createMockStorage() {
  const blocks = new Map();
  const records = new Map();
  /** @type {any[]} */
  const commits = [];
  const metadata = { did: null };

  return {
    async getBlock(/** @type {any} */ cid) {
      return blocks.get(cid) || null;
    },
    async putBlock(/** @type {any} */ cid, /** @type {any} */ data) {
      blocks.set(cid, data);
    },
    async getRecord(/** @type {any} */ uri) {
      const rec = records.get(uri);
      return rec || null;
    },
    async putRecord(
      /** @type {any} */ uri,
      /** @type {any} */ cid,
      /** @type {any} */ _collection,
      /** @type {any} */ _rkey,
      /** @type {any} */ value,
    ) {
      records.set(uri, { cid, value });
    },
    async listRecords() {
      return { records: [], cursor: null };
    },
    async listAllRecords() {
      return [];
    },
    async deleteRecord() {},
    async getLatestCommit() {
      return commits.length > 0 ? commits[commits.length - 1] : null;
    },
    async putCommit(
      /** @type {any} */ seq,
      /** @type {any} */ cid,
      /** @type {any} */ rev,
      /** @type {any} */ prev,
    ) {
      commits.push({ seq, cid, rev, prev });
    },
    async getEvents() {
      return { events: [], cursor: 0 };
    },
    async putEvent() {},
    async getBlob() {
      return null;
    },
    async putBlob() {},
    async deleteBlob() {},
    async listBlobs() {
      return { cids: [], cursor: null };
    },
    async getOrphanedBlobs() {
      return [];
    },
    async linkBlobToRecord() {},
    async unlinkBlobsFromRecord() {},
    async getDid() {
      return metadata.did;
    },
    async setDid(/** @type {any} */ did) {
      metadata.did = did;
    },
    async getHandle() {
      return null;
    },
    async setHandle() {},
    async getPrivateKey() {
      return null;
    },
    async setPrivateKey() {},
    async getKemKey() {
      return null;
    },
    async setKemKey() {},
    async getPreferences() {
      return [];
    },
    async setPreferences() {},
    _getRecords() {
      return records;
    },
    _getCommits() {
      return commits;
    },
  };
}

describe('buildRepositoryCar', () => {
  it('should build a valid CAR with a single record', async () => {
    const { privateKey } = await generateKeyPair();
    const did = 'did:plc:testbuild123';

    const { carBytes, commitCid, rev } = await buildRepositoryCar({
      did,
      privateKey,
      records: [
        {
          collection: 'app.bsky.feed.post',
          rkey: '3abc123',
          record: {
            $type: 'app.bsky.feed.post',
            text: 'Hello World!',
            createdAt: '2024-01-01T00:00:00Z',
          },
        },
      ],
    });

    expect(carBytes).toBeInstanceOf(Uint8Array);
    expect(carBytes.length).toBeGreaterThan(0);
    expect(commitCid).toBeDefined();
    expect(rev).toBeDefined();
  });

  it('should build a valid CAR with multiple records', async () => {
    const { privateKey } = await generateKeyPair();
    const did = 'did:plc:testmulti456';

    const { carBytes, commitCid } = await buildRepositoryCar({
      did,
      privateKey,
      records: [
        {
          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' },
        },
      ],
    });

    expect(carBytes).toBeInstanceOf(Uint8Array);
    expect(commitCid).toBeDefined();
  });

  it('should produce a CAR that can be parsed', async () => {
    const { privateKey } = await generateKeyPair();
    const did = 'did:plc:testparse789';

    const { carBytes, commitCid } = await buildRepositoryCar({
      did,
      privateKey,
      records: [
        {
          collection: 'app.bsky.feed.post',
          rkey: '3test',
          record: {
            $type: 'app.bsky.feed.post',
            text: 'Test',
            createdAt: '2024-01-01T00:00:00Z',
          },
        },
      ],
    });

    // Parse the CAR
    const parsed = parseCarFile(carBytes);

    expect(parsed.roots).toHaveLength(1);
    expect(parsed.roots[0]).toBe(commitCid);
    expect(parsed.blocks.size).toBeGreaterThan(0);

    // Verify commit structure
    const commitBytes = parsed.blocks.get(commitCid);
    expect(commitBytes).toBeDefined();

    const commit = cborDecode(/** @type {Uint8Array} */ (commitBytes));
    expect(commit.did).toBe(did);
    expect(commit.version).toBe(3);
    expect(commit.sig).toBeDefined();
    expect(commit.sig).toBeInstanceOf(Uint8Array);
    expect(commit.sig.length).toBe(64); // ECDSA P-256 signature
  });

  it('should produce a CAR that can be loaded into storage', async () => {
    const { privateKey } = await generateKeyPair();
    const did = 'did:plc:testload101';

    const { carBytes } = await buildRepositoryCar({
      did,
      privateKey,
      records: [
        {
          collection: 'app.bsky.feed.post',
          rkey: '3loadtest',
          record: {
            $type: 'app.bsky.feed.post',
            text: 'Load Test',
            createdAt: '2024-01-01T00:00:00Z',
          },
        },
      ],
    });

    // Load into storage
    const storage = createMockStorage();
    const result = await loadRepositoryFromCar(
      carBytes,
      /** @type {any} */ (storage),
    );

    expect(result.did).toBe(did);
    expect(result.recordCount).toBe(1);
    expect(await storage.getDid()).toBe(did);

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

    // Verify commit was stored
    const commit = await storage.getLatestCommit();
    expect(commit).not.toBeNull();
    expect(commit?.cid).toBe(result.commitCid);
  });

  it('should handle empty records gracefully', async () => {
    const { privateKey } = await generateKeyPair();
    const did = 'did:plc:testempty';

    // Even with no records, should produce valid CAR with just commit
    const { carBytes, commitCid } = await buildRepositoryCar({
      did,
      privateKey,
      records: [],
    });

    expect(carBytes).toBeInstanceOf(Uint8Array);
    expect(commitCid).toBeDefined();

    const parsed = parseCarFile(carBytes);
    expect(parsed.roots[0]).toBe(commitCid);
  });

  it('should produce deterministic MST structure', async () => {
    const { privateKey } = await generateKeyPair();
    const did = 'did:plc:testdeterministic';

    // Records in different order should produce same MST
    const records = [
      { collection: 'c/b', rkey: 'z', record: { val: 3 } },
      { collection: 'a/a', rkey: 'x', record: { val: 1 } },
      { collection: 'b/a', rkey: 'y', record: { val: 2 } },
    ];

    const { carBytes: car1 } = await buildRepositoryCar({
      did,
      privateKey,
      records: [...records],
    });

    // Reverse order
    const { carBytes: car2 } = await buildRepositoryCar({
      did,
      privateKey,
      records: [...records].reverse(),
    });

    // Parse both and compare MST structure (not commits - revisions differ)
    const parsed1 = parseCarFile(car1);
    const parsed2 = parseCarFile(car2);

    // Both should have same number of blocks (records + MST nodes + commit)
    expect(parsed1.blocks.size).toBe(parsed2.blocks.size);
  });
});