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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
272 lines · 8.7 KB Raw History
import { cidToString, createBlobCid } from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import {
  cacheControlFor,
  checkUploadedRef,
  compileGlob,
  planDeploy,
  uploadContentType,
} from '../src/deploy.js';

/** @param {string} content */
const bytes = (content) => new TextEncoder().encode(content);

/** @param {string} content @param {string} mimeType */
async function jsonRef(content, mimeType) {
  const data = bytes(content);
  return {
    $type: /** @type {'blob'} */ ('blob'),
    ref: { $link: cidToString(await createBlobCid(data)) },
    mimeType,
    size: data.length,
  };
}

describe('uploadContentType', () => {
  it('maps extensions and defaults to octet-stream', () => {
    expect(uploadContentType('index.html')).toBe('text/html; charset=utf-8');
    expect(uploadContentType('a/b/app.JS')).toBe(
      'text/javascript; charset=utf-8',
    );
    expect(uploadContentType('photo.webp')).toBe('image/webp');
    expect(uploadContentType('mystery')).toBe('application/octet-stream');
  });
});

describe('planDeploy', () => {
  it('uploads everything on a first deploy and builds the record', async () => {
    const plan = await planDeploy({
      site: 'home',
      files: [
        { path: 'index.html', bytes: bytes('<h1>hi</h1>') },
        { path: '/assets/app.js', bytes: bytes('let x') },
      ],
      existing: null,
      fallback: 'index.html',
      now: '2026-08-06T00:00:00.000Z',
    });
    expect(plan.uploads).toHaveLength(2);
    expect(plan.reused).toEqual([]);
    expect(plan.removed).toEqual([]);
    expect(plan.record).toMatchObject({
      $type: 'dev.pdsjs.site.deploy',
      fallback: 'index.html',
      createdAt: '2026-08-06T00:00:00.000Z',
    });
    const paths = /** @type {Array<{path: string}>} */ (plan.record.files).map(
      (file) => file.path,
    );
    expect(paths).toEqual(['index.html', 'assets/app.js']);
  });

  it('reuses unchanged files by CID and lists removals', async () => {
    const unchanged = await jsonRef('same bytes', 'text/css; charset=utf-8');
    const plan = await planDeploy({
      site: 'home',
      files: [
        { path: 'style.css', bytes: bytes('same bytes') },
        { path: 'new.html', bytes: bytes('fresh') },
      ],
      existing: {
        value: {
          files: [
            {
              path: 'style.css',
              blob: unchanged,
              cacheControl: 'public, max-age=31536000, immutable',
            },
            { path: 'gone.html', blob: await jsonRef('old', 'text/html') },
          ],
        },
      },
    });
    expect(plan.reused).toEqual(['style.css']);
    expect(plan.uploads.map((upload) => upload.path)).toEqual(['new.html']);
    expect(plan.removed).toEqual(['gone.html']);
    const kept =
      /** @type {Array<{path: string, blob: {ref: {$link: string}}, cacheControl?: string}>} */ (
        plan.record.files
      ).find((file) => file.path === 'style.css');
    // The existing ref rides along verbatim, cacheControl included
    expect(kept?.blob).toBe(unchanged);
    expect(kept?.cacheControl).toBe('public, max-age=31536000, immutable');
  });

  it('re-uploads when bytes changed', async () => {
    const old = await jsonRef('version one', 'text/html; charset=utf-8');
    const plan = await planDeploy({
      site: 'home',
      files: [{ path: 'index.html', bytes: bytes('version two') }],
      existing: { value: { files: [{ path: 'index.html', blob: old }] } },
    });
    expect(plan.reused).toEqual([]);
    expect(plan.uploads).toHaveLength(1);
    expect(plan.uploads[0].cid).not.toBe(old.ref.$link);
  });

  it('rejects invalid site names', async () => {
    await expect(
      planDeploy({ site: 'Not_A_Label', files: [], existing: null }),
    ).rejects.toThrow(/DNS label/);
  });

  it('aborts before uploading when a file exceeds the blob limit', async () => {
    await expect(
      planDeploy({
        site: 'home',
        files: [
          { path: 'small.txt', bytes: bytes('ok') },
          { path: 'huge.bin', bytes: new Uint8Array(101) },
        ],
        existing: null,
        blobLimit: 100,
      }),
    ).rejects.toThrow(/huge\.bin \(101 bytes\)/);
  });

  it('aborts when the manifest outgrows the record ceiling', async () => {
    const files = Array.from({ length: 6000 }, (_, i) => ({
      path: `deep/nested/directory/structure/file-number-${i}.html`,
      bytes: bytes(`content ${i}`),
    }));
    await expect(
      planDeploy({ site: 'home', files, existing: null }),
    ).rejects.toThrow(/record ceiling/);
  }, 60_000);

  it('rejects a fallback that is not deployed', async () => {
    await expect(
      planDeploy({
        site: 'home',
        files: [{ path: 'index.html', bytes: bytes('hi') }],
        existing: null,
        fallback: 'missing.html',
      }),
    ).rejects.toThrow(/not among/);
  });

  it('rejects duplicate paths', async () => {
    await expect(
      planDeploy({
        site: 'home',
        files: [
          { path: 'a.txt', bytes: bytes('one') },
          { path: 'a.txt', bytes: bytes('two') },
        ],
        existing: null,
      }),
    ).rejects.toThrow(/Duplicate/);
  });

  it('carries an existing base ref through untouched', async () => {
    const base = {
      uri: 'at://did:plc:x/dev.pdsjs.site.deploy/assets',
      cid: 'b1',
    };
    const plan = await planDeploy({
      site: 'home',
      files: [{ path: 'index.html', bytes: bytes('hi') }],
      existing: { value: { files: [], base } },
    });
    expect(plan.record.base).toBe(base);
  });
});

describe('compileGlob', () => {
  it('keeps * within a segment and lets ** cross them', () => {
    expect(compileGlob('*.css').test('style.css')).toBe(true);
    expect(compileGlob('*.css').test('assets/style.css')).toBe(false);
    expect(compileGlob('assets/**').test('assets/deep/app.js')).toBe(true);
    expect(compileGlob('**/*.js').test('assets/deep/app.js')).toBe(true);
    expect(compileGlob('index.html').test('index2.html')).toBe(false);
  });
});

describe('cacheControlFor', () => {
  const rules = [
    { value: 'public, max-age=60' },
    { glob: 'assets/**', value: 'public, max-age=31536000, immutable' },
  ];

  it('applies the last matching rule, globless rules matching everything', () => {
    expect(cacheControlFor('index.html', rules)).toBe('public, max-age=60');
    expect(cacheControlFor('assets/app-abc123.js', rules)).toBe(
      'public, max-age=31536000, immutable',
    );
    expect(cacheControlFor('anything', [])).toBeUndefined();
  });
});

describe('planDeploy cache rules', () => {
  it('sets matched files, leaving unmatched ones on their record value', async () => {
    const kept = await jsonRef('body{}', 'text/css; charset=utf-8');
    const plan = await planDeploy({
      site: 'home',
      files: [
        { path: 'index.html', bytes: bytes('<h1>hi</h1>') },
        { path: 'style.css', bytes: bytes('body{}') },
      ],
      existing: {
        value: {
          files: [
            {
              path: 'style.css',
              blob: kept,
              cacheControl: 'public, max-age=5',
            },
          ],
        },
      },
      cacheRules: [{ glob: '*.html', value: 'public, max-age=60' }],
    });
    const entries =
      /** @type {Array<{path: string, cacheControl?: string}>} */ (
        plan.record.files
      );
    expect(entries.find((e) => e.path === 'index.html')?.cacheControl).toBe(
      'public, max-age=60',
    );
    // Unmatched by any rule: the reused file keeps what the record said
    expect(entries.find((e) => e.path === 'style.css')?.cacheControl).toBe(
      'public, max-age=5',
    );
  });

  it('overrides a carried-forward value when a rule matches it', async () => {
    const kept = await jsonRef('body{}', 'text/css; charset=utf-8');
    const plan = await planDeploy({
      site: 'home',
      files: [{ path: 'style.css', bytes: bytes('body{}') }],
      existing: {
        value: {
          files: [
            {
              path: 'style.css',
              blob: kept,
              cacheControl: 'public, max-age=5',
            },
          ],
        },
      },
      cacheRules: [{ value: 'public, max-age=31536000, immutable' }],
    });
    const entries =
      /** @type {Array<{path: string, cacheControl?: string}>} */ (
        plan.record.files
      );
    expect(entries[0].cacheControl).toBe('public, max-age=31536000, immutable');
    expect(plan.uploads).toHaveLength(0);
  });
});

describe('checkUploadedRef', () => {
  it('passes matching CIDs through and rejects mismatches', async () => {
    const ref = await jsonRef('content', 'text/plain');
    expect(checkUploadedRef({ path: 'a.txt', cid: ref.ref.$link }, ref)).toBe(
      ref,
    );
    expect(() =>
      checkUploadedRef({ path: 'a.txt', cid: 'bafyother' }, ref),
    ).toThrow(/Not deploying/);
  });
});