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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
188 lines · 5.9 KB Raw History
// The git extension against a fake host: the write guard reads the config
// record from the public repo or a space, and smart HTTP mounts only when
// asked for.
import { cborEncodeDagCbor } from '@pdsjs/core/repo';
import { describe, expect, it, vi } from 'vitest';
import {
  createGitExtension,
  createGitGuardExtension,
} from '../src/extension.js';

const DID = 'did:plc:gitextension';
const SPACE = 'at://did:plc:gitextension/dev.pdsjs.space/abc';
const CONFIG = 'dev.pdsjs.git.config';

/**
 * @param {Object} [options]
 * @param {boolean} [options.withSpaces]
 */
function createHost({ withSpaces = false } = {}) {
  const records = new Map([
    [
      `at://${DID}/${CONFIG}/repo`,
      cborEncodeDagCbor({ protectedBranches: ['main'] }),
    ],
  ]);
  const spaceRecords = new Map([
    [`${CONFIG}/repo`, cborEncodeDagCbor({ protectedBranches: ['release'] })],
  ]);
  const host = {
    actorStorage: {
      getRecord: vi.fn(async (/** @type {string} */ uri) => {
        const value = records.get(uri);
        return value ? { value } : null;
      }),
    },
    blobs: { get: vi.fn(async () => null) },
    spaceStorage: withSpaces
      ? {
          getSpaceRecord: vi.fn(
            async (
              /** @type {string} */ _space,
              /** @type {string} */ collection,
              /** @type {string} */ rkey,
            ) => {
              const value = spaceRecords.get(`${collection}/${rkey}`);
              return value ? { cid: 'bafy', value } : null;
            },
          ),
        }
      : undefined,
    readOnly: false,
    getDid: async () => DID,
    xrpc: vi.fn(async () => new Response('{}')),
    createCommit: vi.fn(),
    accountApi: vi.fn(),
  };
  return /** @type {import('@pdsjs/core/ports').ExtensionHost} */ (
    /** @type {unknown} */ (host)
  );
}

/** @param {string} branch @param {string} sha */
const repo = (branch, sha) => ({
  name: 'repo',
  createdAt: '2026-01-01T00:00:00.000Z',
  refs: [{ name: `refs/heads/${branch}`, sha }],
  bundles: [],
});
const emptyRepo = {
  name: 'repo',
  createdAt: '2026-01-01T00:00:00.000Z',
  refs: [],
  bundles: [],
};

describe('createGitExtension', () => {
  it('guards repo and config writes without any option', () => {
    const extension = createGitExtension()(createHost());
    expect(extension.name).toBe('git');
    expect(extension.writeGuard?.collections).toEqual([
      'dev.pdsjs.git.repo',
      CONFIG,
    ]);
    expect(extension.handle).toBeUndefined();
    expect(extension.prefixes).toBeUndefined();
  });

  it('registers the XRPC methods, the bundle-chain reads with a browser', () => {
    const routes = Object.keys(createGitExtension()(createHost()).routes ?? {});
    expect(routes).toContain('/xrpc/dev.pdsjs.git.listRepos');
    expect(routes).toContain('/xrpc/dev.pdsjs.git.getTree');
    const noBrowse = Object.keys(
      createGitExtension({ browse: false })(createHost()).routes ?? {},
    );
    expect(noBrowse).toContain('/xrpc/dev.pdsjs.git.listRepos');
    expect(noBrowse).not.toContain('/xrpc/dev.pdsjs.git.getTree');
  });

  it('serves smart HTTP under /git when asked', async () => {
    const host = createHost();
    const extension = createGitExtension({ http: true })(host);
    expect(extension.prefixes).toEqual(['/git']);
    const miss = await extension.handle?.(
      new Request('https://pds.example.com/other'),
      new URL('https://pds.example.com/other'),
    );
    expect(miss).toBeNull();
  });

  it('reads the public config record to refuse a protected ref deletion', async () => {
    const host = createHost();
    const guard = createGitExtension()(host).writeGuard;
    await expect(
      guard?.check({
        space: null,
        collection: 'dev.pdsjs.git.repo',
        rkey: 'repo',
        prevValue: repo('main', 'a'.repeat(40)),
        nextValue: emptyRepo,
      }),
    ).rejects.toThrow(/protected/);
    expect(host.actorStorage.getRecord).toHaveBeenCalledWith(
      `at://${DID}/${CONFIG}/repo`,
    );
  });

  it('reads a space config record through the host space storage', async () => {
    const host = createHost({ withSpaces: true });
    const guard = createGitExtension()(host).writeGuard;
    await expect(
      guard?.check({
        space: SPACE,
        collection: 'dev.pdsjs.git.repo',
        rkey: 'repo',
        prevValue: repo('release', 'a'.repeat(40)),
        nextValue: emptyRepo,
      }),
    ).rejects.toThrow(/protected/);
    expect(host.spaceStorage?.getSpaceRecord).toHaveBeenCalledWith(
      SPACE,
      CONFIG,
      'repo',
    );
  });

  it('holds no rules for a space write when spaces are off', async () => {
    const guard = createGitExtension()(createHost()).writeGuard;
    await expect(
      guard?.check({
        space: SPACE,
        collection: 'dev.pdsjs.git.repo',
        rkey: 'repo',
        prevValue: repo('release', 'a'.repeat(40)),
        nextValue: emptyRepo,
      }),
    ).resolves.toBeUndefined();
  });
});

describe('createGitGuardExtension', () => {
  it('carries the write guard and nothing else', () => {
    const host = createHost();
    const extension = createGitGuardExtension()(host);
    expect(extension.name).toBe('git');
    expect(extension.writeGuard).toBeDefined();
    expect(extension.routes).toBeUndefined();
    expect(extension.handle).toBeUndefined();
    expect(extension.prefixes).toBeUndefined();
    expect(extension.collections).toContain('dev.pdsjs.git.repo');
  });

  it('enforces branch protection at the record write', async () => {
    const host = createHost();
    const guard = createGitGuardExtension()(host).writeGuard;
    await expect(
      guard?.check({
        space: null,
        collection: 'dev.pdsjs.git.repo',
        rkey: 'repo',
        prevValue: repo('main', 'a'.repeat(40)),
        nextValue: emptyRepo,
      }),
    ).rejects.toThrow(/protected/);
    expect(host.actorStorage.getRecord).toHaveBeenCalledWith(
      `at://${DID}/${CONFIG}/repo`,
    );
  });
});