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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
193 lines · 5.7 KB Raw History
import { describe, expect, it } from 'vitest';
import {
  accessLabel,
  appAccessLabel,
  bytesToHex,
  collectionsOf,
  fileSize,
  nsidDomain,
  policyLabel,
  rkeyTime,
  shortDid,
  spaceParts,
  until,
} from '#/lib/format.js';

describe('spaceParts', () => {
  it('reads the three components a heading shows', () => {
    expect(
      spaceParts('at://did:plc:abcdef/space/com.example.forum/general'),
    ).toEqual({
      spaceDid: 'did:plc:abcdef',
      spaceType: 'com.example.forum',
      skey: 'general',
    });
  });

  it('refuses a public AT-URI, which addresses a repo rather than a space', () => {
    expect(() =>
      spaceParts('at://did:plc:abcdef/app.bsky.feed.post/3k2a'),
    ).toThrow();
  });
});

describe('shortDid', () => {
  it('leaves a short DID whole', () => {
    expect(shortDid('did:web:example.com')).toBe('did:web:example.com');
  });

  it('keeps both ends, so two DIDs from one directory read apart', () => {
    const shortened = shortDid('did:plc:c4igoshkxhzw62eme3iltpdi');
    expect(shortened.startsWith('did:plc:c4igosh')).toBe(true);
    expect(shortened.endsWith('ltpdi')).toBe(true);
  });
});

describe('rkeyTime', () => {
  it('reads the write time out of a TID', () => {
    const at = rkeyTime('3mtjzqmmgxsrb');
    expect(at?.getUTCFullYear()).toBe(2026);
  });

  it('answers null for a key that is not a TID', () => {
    expect(rkeyTime('self')).toBeNull();
  });
});

describe('until', () => {
  // Each offset carries most of a unit in hand, because the clock moves between
  // building the argument and reading it, and the count is floored.
  it('counts down in the coarsest unit that still says something', () => {
    expect(until(Date.now() + 45_900)).toBe('in 45 seconds');
    expect(until(Date.now() + 2 * 3_600_000 + 60_000)).toBe('in 2 hours');
  });

  // A delegation token lives sixty seconds, so a countdown that stopped at
  // minutes would show every one of them as nothing.
  it('has seconds as a unit', () => {
    expect(until(Date.now() + 1900)).toBe('in 1 second');
  });

  it('says a past time has expired', () => {
    expect(until(Date.now() - 1000)).toBe('expired');
  });

  it('answers empty for no time at all', () => {
    expect(until(null)).toBe('');
  });
});

describe('fileSize', () => {
  it('names the unit the number is readable in', () => {
    expect(fileSize(70)).toBe('70 B');
    expect(fileSize(2048)).toBe('2 KB');
    expect(fileSize(3 * 1024 * 1024)).toBe('3.0 MB');
  });
});

describe('bytesToHex', () => {
  it('renders a commit hash the way another implementation prints it', () => {
    expect(bytesToHex({ $bytes: 'AAECqv8' })).toBe('000102aaff');
  });

  it('answers empty for a field that carries no bytes', () => {
    expect(bytesToHex(null)).toBe('');
    expect(bytesToHex(undefined)).toBe('');
  });
});

describe('collectionsOf', () => {
  const records = [
    { collection: 'b.example.note', rkey: '3mtjzqmmgxsrb' },
    { collection: 'a.example.pin', rkey: '3mtjzqmmwlsrb' },
    { collection: 'b.example.note', rkey: 'self' },
  ];

  it('counts each collection and sorts by name', () => {
    expect(collectionsOf(records).map((c) => [c.collection, c.count])).toEqual([
      ['a.example.pin', 1],
      ['b.example.note', 2],
    ]);
  });

  it('takes the latest write time from the TIDs, ignoring keys that carry none', () => {
    const notes = collectionsOf(records)[1];
    expect(notes.lastWriteAt).toBe(rkeyTime('3mtjzqmmgxsrb')?.getTime());
  });

  it('leaves lastWriteAt null when no key is a TID', () => {
    expect(
      collectionsOf([{ collection: 'a.example.x', rkey: 'self' }])[0]
        .lastWriteAt,
    ).toBeNull();
  });
});

describe('policyLabel', () => {
  it('names the policy', () => {
    expect(
      policyLabel({ policy: { $type: 'com.atproto.memberspace.policy' } }),
    ).toBe('Members on a list');
  });

  it('says so plainly for a policy it does not know', () => {
    expect(policyLabel({})).toBe('Unknown');
  });
});

describe('accessLabel', () => {
  const members = { $type: 'com.atproto.memberspace.policy' };

  it('reads public access off a memberspace', () => {
    const config = { policy: { ...members, publicRead: true } };
    expect(accessLabel(config, 'read')).toBe('Anyone');
    expect(accessLabel(config, 'write')).toBe('Members who may write');
  });

  it('names the policy for one with no flags', () => {
    expect(
      accessLabel(
        { policy: { $type: 'com.atproto.personalspace.policy' } },
        'read',
      ),
    ).toBe('The authority alone');
  });
});

describe('appAccessLabel', () => {
  it('counts an allow list', () => {
    expect(
      appAccessLabel({
        policy: {
          $type: 'com.atproto.memberspace.policy',
          allowedApps: ['https://one.example', 'https://two.example'],
        },
      }),
    ).toBe('2 clients on an allow list');
  });

  it('treats an absent list as open, which is the documented default', () => {
    expect(appAccessLabel({})).toBe('Any client');
  });
});

describe('nsidDomain', () => {
  // A space type is an NSID, and the domain it reverses to is the app the
  // space belongs to, which is what serves the icon.
  it('reverses the first two segments', () => {
    expect(nsidDomain('my.bulletin.board')).toBe('bulletin.my');
    expect(nsidDomain('social.grain.gallery')).toBe('grain.social');
    expect(nsidDomain('app.bulleted.space')).toBe('bulleted.app');
  });

  // Two segments, not the whole authority: a favicon answers at `bsky.app`,
  // never at `feed.bsky.app`.
  it('stops at the registrable domain however deep the NSID goes', () => {
    expect(nsidDomain('app.bsky.feed.post')).toBe('bsky.app');
    expect(nsidDomain('dev.pdsjs.drive.space')).toBe('pdsjs.dev');
  });

  it('leaves something that is not an NSID alone', () => {
    expect(nsidDomain('board')).toBe('board');
  });
});