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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
117 lines · 3.6 KB Raw History
// Preferences handler tests - auth, read-only, and the array shape
import { describe, expect, it } from 'vitest';
import { createPreferencesHandlers } from '../packages/core/src/handlers/xrpc-preferences.js';

const DID = 'did:plc:preferencestestaccount';
const FULL = { did: DID, scope: 'com.atproto.access' };

/** @param {{readOnly?: boolean, stored?: unknown[]}} [options] */
function createPreferences({ readOnly = false, stored = [] } = {}) {
  let preferences = stored;
  const handlers = createPreferencesHandlers({
    actorStorage: /** @type {any} */ ({
      async getPreferences() {
        return preferences;
      },
      async setPreferences(/** @type {unknown[]} */ next) {
        preferences = next;
      },
    }),
    readOnly,
    readOnlyError: () =>
      Response.json(
        { error: 'AuthenticationRequired', message: 'This PDS is read-only' },
        { status: 401 },
      ),
  });
  return { handlers, read: () => preferences };
}

const GET = '/xrpc/app.bsky.actor.getPreferences';
const PUT = '/xrpc/app.bsky.actor.putPreferences';

/**
 * @param {ReturnType<typeof createPreferencesHandlers>} handlers
 * @param {string} path
 * @param {{body?: Object, auth?: {did: string, scope: string}|null}} [options]
 */
async function call(handlers, path, { body, auth = null } = {}) {
  const route = handlers.routes[path];
  const url = new URL(`https://pds.example.com${path}`);
  /** @type {RequestInit} */
  const init = { method: body ? 'POST' : 'GET' };
  if (body) init.body = JSON.stringify(body);
  const handler =
    /** @type {(request: Request, url: URL, auth: unknown) => Promise<Response>} */ (
      route.handler
    );
  return handler(new Request(url, init), url, auth);
}

describe('getPreferences', () => {
  it('answers with what is stored', async () => {
    const { handlers } = createPreferences({
      stored: [{ $type: 'app.bsky.actor.defs#adultContentPref' }],
    });
    const body = await (await call(handlers, GET, { auth: FULL })).json();

    expect(body.preferences).toHaveLength(1);
  });

  it('needs a session', async () => {
    const { handlers } = createPreferences();
    const response = await call(handlers, GET);

    expect(response.status).toBe(401);
    expect((await response.json()).error).toBe('AuthenticationRequired');
  });
});

describe('putPreferences', () => {
  it('replaces the stored array', async () => {
    const { handlers, read } = createPreferences({ stored: ['old'] });
    const response = await call(handlers, PUT, {
      auth: FULL,
      body: { preferences: ['new'] },
    });

    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({});
    expect(read()).toEqual(['new']);
  });

  it('refuses a read-only server', async () => {
    const { handlers, read } = createPreferences({
      readOnly: true,
      stored: ['old'],
    });
    const response = await call(handlers, PUT, {
      auth: FULL,
      body: { preferences: ['new'] },
    });

    expect(response.status).toBe(401);
    expect(read()).toEqual(['old']);
  });

  it('needs a session', async () => {
    const { handlers } = createPreferences();
    const response = await call(handlers, PUT, {
      body: { preferences: [] },
    });

    expect(response.status).toBe(401);
  });

  it('refuses anything that is not an array', async () => {
    const { handlers, read } = createPreferences({ stored: ['old'] });
    const response = await call(handlers, PUT, {
      auth: FULL,
      body: { preferences: { nope: true } },
    });

    expect(response.status).toBe(400);
    expect((await response.json()).message).toMatch(/must be an array/);
    expect(read()).toEqual(['old']);
  });
});