chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createOAuthClient } from '../src/index.js';
class StorageMock {
values = new Map();
/** @param {string} key */
getItem(key) {
return this.values.get(key) ?? null;
}
/** @param {string} key @param {string} value */
setItem(key, value) {
this.values.set(key, String(value));
}
/** @param {string} key */
removeItem(key) {
this.values.delete(key);
}
}
/** @type {ReturnType<typeof vi.fn>} */
let fetchMock;
/** @returns {import('../src/oauth.js').OAuthClient} */
function client() {
const dpop = {
dpopKeyPair: vi.fn(),
forgetDpopKey: vi.fn(async () => {}),
sha256Base64Url: vi.fn(async () => 'challenge'),
dpopProof: vi.fn(async () => 'proof'),
nonceFor: vi.fn(() => undefined),
rememberNonce: vi.fn(),
};
return createOAuthClient({
dpop: /** @type {any} */ (dpop),
resolveIdentity: async () => ({
did: 'did:plc:alice',
handle: 'alice.example',
pds: 'https://pds.example',
}),
scope: 'atproto',
storagePrefix: 'oauth-client-test',
});
}
beforeEach(() => {
vi.stubGlobal('window', {
location: {
origin: 'http://localhost:5173',
hostname: 'localhost',
assign: vi.fn(() => {
throw new Error('navigation');
}),
},
});
vi.stubGlobal('localStorage', new StorageMock());
vi.stubGlobal('sessionStorage', new StorageMock());
fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
});
describe('createOAuthClient', () => {
it('creates a loopback client id from the requested scope', () => {
const oauth = client();
expect(oauth.redirectUri()).toBe('http://localhost:5173/callback');
expect(oauth.clientId('atproto read')).toBe(
'http://localhost?redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fcallback&scope=atproto+read',
);
});
it('isolates and reads its configured session storage', () => {
const oauth = client();
localStorage.setItem(
'oauth-client-test:session',
JSON.stringify({ did: 'did:plc:alice' }),
);
localStorage.setItem(
'other-app:session',
JSON.stringify({ did: 'did:plc:other' }),
);
expect(oauth.currentSession()).toEqual({ did: 'did:plc:alice' });
});
it('stores the exact client id and scope used by a sign-in', async () => {
fetchMock
.mockResolvedValueOnce(
new Response(
JSON.stringify({ authorization_servers: ['https://auth.example'] }),
{ headers: { 'content-type': 'application/json' } },
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
issuer: 'https://auth.example',
pushed_authorization_request_endpoint:
'https://auth.example/oauth/par',
authorization_endpoint: 'https://auth.example/oauth/authorize',
token_endpoint: 'https://auth.example/oauth/token',
}),
{ headers: { 'content-type': 'application/json' } },
),
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ request_uri: 'request-1' }), {
headers: { 'content-type': 'application/json' },
}),
);
const oauth = client();
await expect(
oauth.beginSignIn('alice.example', {
scope: 'atproto read',
returnTo: '/records',
}),
).rejects.toThrow('navigation');
const pending = JSON.parse(
sessionStorage.getItem('oauth-client-test:pending') || '',
);
expect(pending.scope).toBe('atproto read');
expect(pending.clientId).toBe(oauth.clientId('atproto read'));
expect(pending.returnTo).toBe('/records');
});
});