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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
196 lines · 5.8 KB Raw History
import { describe, expect, it } from 'vitest';
import { runSpaceStorageConformance } from '../../../test/helpers/space-storage-conformance.js';
import { createSpaceStorage } from '../src/space.js';

runSpaceStorageConformance('storage-sqlite', async () => {
  const Database = (await import('better-sqlite3')).default;
  return createSpaceStorage(new Database(':memory:'));
});

describe('opening a database from before space revisions', () => {
  it('sequences each recorded writer at its repo rev', async () => {
    const Database = (await import('better-sqlite3')).default;
    const db = new Database(':memory:');
    db.exec(`CREATE TABLE space_writer (
      space TEXT NOT NULL,
      did TEXT NOT NULL,
      rev TEXT NOT NULL,
      hash BLOB NOT NULL,
      PRIMARY KEY (space, did)
    )`);
    db.prepare(
      'INSERT INTO space_writer (space, did, rev, hash) VALUES (?, ?, ?, ?)',
    ).run(
      'at://did:plc:a/space/x.y.z/s',
      'did:plc:w',
      '3lkaaaaaaaa22',
      Buffer.alloc(32),
    );

    const store = createSpaceStorage(db);
    const { writers, cursor } = await store.listSpaceWriters(
      'at://did:plc:a/space/x.y.z/s',
      null,
      10,
    );
    expect(writers).toEqual([
      expect.objectContaining({
        did: 'did:plc:w',
        repoRev: '3lkaaaaaaaa22',
        spaceRev: '3lkaaaaaaaa22',
      }),
    ]);
    expect(cursor).toBe('3lkaaaaaaaa22');
    expect(await store.getSpaceRevHead('at://did:plc:a/space/x.y.z/s')).toBe(
      '3lkaaaaaaaa22',
    );
  });
});

describe('opening a database from before space policies', () => {
  const A = 'at://did:plc:a/space/x.y.z/members';
  const B = 'at://did:plc:a/space/x.y.z/public';
  const C = 'at://did:plc:a/space/x.y.z/managed';
  const D = 'at://did:plc:b/space/x.y.z/joined';

  async function openLegacy() {
    const Database = (await import('better-sqlite3')).default;
    const db = new Database(':memory:');
    db.exec(`CREATE TABLE space (
      uri TEXT PRIMARY KEY,
      space_did TEXT NOT NULL,
      space_type TEXT NOT NULL,
      is_owner INTEGER NOT NULL,
      read_policy TEXT NOT NULL DEFAULT 'member-list',
      read_managing_app TEXT,
      write_policy TEXT NOT NULL DEFAULT 'member-list',
      write_managing_app TEXT,
      app_access_type TEXT NOT NULL DEFAULT 'open',
      app_allowed TEXT NOT NULL DEFAULT '[]',
      created_at TEXT NOT NULL,
      deleted_at TEXT
    );
    CREATE TABLE space_member (
      space TEXT NOT NULL,
      did TEXT NOT NULL,
      can_read INTEGER NOT NULL DEFAULT 1,
      can_write INTEGER NOT NULL DEFAULT 1,
      PRIMARY KEY (space, did)
    );`);
    const insert = db.prepare(
      `INSERT INTO space (uri, space_did, space_type, is_owner, read_policy,
       read_managing_app, write_policy, write_managing_app, app_access_type,
       app_allowed, created_at) VALUES (?, ?, 'x.y.z', ?, ?, ?, ?, ?, ?, ?, '2026-10-01T00:00:00.000Z')`,
    );
    insert.run(
      A,
      'did:plc:a',
      1,
      'member-list',
      null,
      'member-list',
      null,
      'open',
      '[]',
    );
    insert.run(
      B,
      'did:plc:a',
      1,
      'public',
      null,
      'member-list',
      null,
      'allowList',
      '["https://app.example/c.json"]',
    );
    insert.run(
      C,
      'did:plc:a',
      1,
      'member-list',
      null,
      'managing-app',
      'did:web:app.example#svc',
      'allowList',
      '["https://app.example/c.json"]',
    );
    insert.run(
      D,
      'did:plc:b',
      0,
      'member-list',
      null,
      'member-list',
      null,
      'open',
      '[]',
    );
    const member = db.prepare(
      'INSERT INTO space_member (space, did, can_read, can_write) VALUES (?, ?, ?, ?)',
    );
    member.run(A, 'did:plc:m', 1, 0);
    member.run(C, 'did:plc:m', 1, 1);
    return { db, store: createSpaceStorage(db) };
  }

  it('makes a memberspace of a member-list space, keeping its members', async () => {
    const { store } = await openLegacy();
    expect((await store.getSpace(A))?.policy).toEqual({
      $type: 'com.atproto.memberspace.policy',
      publicRead: false,
      publicWrite: false,
      publishPolicy: false,
      publishMembers: false,
    });
    expect(await store.getMember(A, 'did:plc:m')).toEqual({
      read: true,
      write: false,
    });
  });

  it('carries public access and an app allow list into the memberspace', async () => {
    const { store } = await openLegacy();
    expect((await store.getSpace(B))?.policy).toEqual({
      $type: 'com.atproto.memberspace.policy',
      publicRead: true,
      publicWrite: false,
      allowedApps: ['https://app.example/c.json'],
      publishPolicy: false,
      publishMembers: false,
    });
  });

  it('makes a managedspace of a managing app on either side, dropping its members', async () => {
    const { store } = await openLegacy();
    expect((await store.getSpace(C))?.policy).toEqual({
      $type: 'com.atproto.managedspace.policy',
      managingApp: 'did:web:app.example#svc',
      publishPolicy: false,
    });
    expect(await store.getMember(C, 'did:plc:m')).toBeNull();
  });

  it('gives a space another authority governs no policy', async () => {
    const { store } = await openLegacy();
    expect((await store.getSpace(D))?.policy).toBeNull();
  });

  it('converts once, so a second open changes nothing', async () => {
    const { db, store } = await openLegacy();
    await store.putSpace({
      .../** @type {import('@pdsjs/core/ports').SpaceRow} */ (
        await store.getSpace(A)
      ),
      policy: { $type: 'com.atproto.personalspace.policy' },
    });
    const again = createSpaceStorage(db);
    expect((await again.getSpace(A))?.policy).toEqual({
      $type: 'com.atproto.personalspace.policy',
    });
    expect(await again.getMember(A, 'did:plc:m')).toEqual({
      read: true,
      write: false,
    });
  });
});