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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
144 lines · 4.2 KB Raw History
import {
  CID,
  cborEncodeDagCbor,
  cidToString,
  createBlobCid,
  createCid,
} from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import { createSiteBrowser } from '../src/browser.js';

const DID = 'did:plc:browsertest';

/** @param {Array<{name: string, files: Array<{path: string, size: number}>, createdAt?: string}>} sites */
async function storageWith(sites) {
  /** @type {Array<{uri: string, cid: string, value: Uint8Array}>} */
  const rows = [];
  for (const site of sites) {
    const files = [];
    for (const file of site.files) {
      const data = new Uint8Array(file.size);
      files.push({
        path: file.path,
        blob: {
          $type: 'blob',
          ref: new CID(await createBlobCid(data)),
          mimeType: 'application/octet-stream',
          size: file.size,
        },
      });
    }
    const value = cborEncodeDagCbor({
      $type: 'dev.pdsjs.site.deploy',
      files,
      createdAt: site.createdAt ?? '2026-08-06T00:00:00.000Z',
    });
    rows.push({
      uri: `at://${DID}/dev.pdsjs.site.deploy/${site.name}`,
      cid: cidToString(await createCid(value)),
      value,
    });
  }
  return /** @type {any} */ ({
    listRecords: async (
      /** @type {string} */ _collection,
      /** @type {string|null} */ cursor,
      /** @type {number} */ limit,
    ) => {
      const start = cursor ? Number(cursor) : 0;
      const records = rows.slice(start, start + limit);
      const next = start + records.length;
      return { records, cursor: next < rows.length ? String(next) : null };
    },
    getRecord: async (/** @type {string} */ uri) =>
      rows.find((row) => row.uri === uri) ?? null,
  });
}

describe('createSiteBrowser', () => {
  it('throws on missing ctx entries', () => {
    expect(() => createSiteBrowser(/** @type {any} */ ({}))).toThrow(
      /actorStorage/,
    );
  });

  it('lists sites with counts, sizes, urls, and servability', async () => {
    const browser = createSiteBrowser({
      actorStorage: await storageWith([
        {
          name: 'home',
          files: [
            { path: 'index.html', size: 1000 },
            { path: 'style.css', size: 500 },
          ],
        },
        { name: 'blog', files: [{ path: 'index.html', size: 250 }] },
        { name: 'Bad_Name', files: [] },
      ]),
      getDid: async () => DID,
      siteDomain: 'example.com',
    });

    expect(browser.domain).toBe('example.com');
    expect(browser.apexName).toBe('home');

    const sites = await browser.listSites();
    expect(sites).toHaveLength(3);
    const home = sites.find((site) => site.name === 'home');
    expect(home).toMatchObject({
      url: 'https://example.com',
      fileCount: 2,
      totalSize: 1500,
      deployedAt: '2026-08-06T00:00:00.000Z',
      servable: true,
    });
    expect(home?.cid).toMatch(/^b/);
    expect(sites.find((site) => site.name === 'blog')).toMatchObject({
      url: 'https://blog.example.com',
      servable: true,
    });
    expect(sites.find((site) => site.name === 'Bad_Name')).toMatchObject({
      servable: false,
    });
  });

  it("lists a site's files sorted by path, and null for a missing site", async () => {
    const browser = createSiteBrowser({
      actorStorage: await storageWith([
        {
          name: 'home',
          files: [
            { path: 'style.css', size: 500 },
            { path: 'index.html', size: 1000 },
          ],
        },
      ]),
      getDid: async () => DID,
      siteDomain: 'example.com',
    });
    const files = await browser.listFiles('home');
    expect(files?.map((file) => file.path)).toEqual([
      'index.html',
      'style.css',
    ]);
    expect(files?.[0]).toMatchObject({
      size: 1000,
      contentType: 'application/octet-stream',
    });
    expect(files?.[0].cid).toMatch(/^b/);
    expect(await browser.listFiles('nope')).toBeNull();
  });

  it('pages through more than one listRecords call', async () => {
    const many = Array.from({ length: 150 }, (_, i) => ({
      name: `site-${i}`,
      files: [],
    }));
    const browser = createSiteBrowser({
      actorStorage: await storageWith(many),
      getDid: async () => DID,
      siteDomain: 'example.com',
    });
    expect(await browser.listSites()).toHaveLength(150);
  });
});