chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// Read-only mode tests
import { beforeEach, describe, expect, it } from 'vitest';
import { PersonalDataServer } from '../packages/core/src/pds.js';
// Create minimal mock storage
function createMockStorage() {
return {
async getRecord() {
return null;
},
async putRecord() {},
async listRecords() {
return { records: [], cursor: null };
},
async listAllRecords() {
return [];
},
async deleteRecord() {},
async getBlock() {
return null;
},
async putBlock() {},
async getLatestCommit() {
return { seq: 1, cid: 'bafytest', rev: '3abc' };
},
async putCommit() {},
async getEvents() {
return { events: [], cursor: 0 };
},
async putEvent() {},
async getLatestSeq() {
return 1;
},
async getAccountStatus() {
return 'active';
},
async setAccountStatus() {},
async getBlob() {
return null;
},
async putBlob() {},
async deleteBlob() {},
async listBlobs() {
return { cids: [], cursor: null };
},
async getOrphanedBlobs() {
return [];
},
async linkBlobToRecord() {},
async unlinkBlobsFromRecord() {},
async getDid() {
return 'did:plc:test123';
},
async setDid() {},
async getHandle() {
return 'test.handle';
},
async setHandle() {},
async getPrivateKey() {
return null;
},
async setPrivateKey() {},
async getKemKey() {
return null;
},
async setKemKey() {},
async getPreferences() {
return [];
},
async setPreferences() {},
};
}
function createMockSharedStorage() {
return {
async getActor() {
return null;
},
async resolveHandle() {
return null;
},
async putActor() {},
async deleteActor() {},
async getOAuthRequest() {
return null;
},
async putOAuthRequest() {},
async deleteOAuthRequest() {},
async getOAuthToken() {
return null;
},
async putOAuthToken() {},
async deleteOAuthToken() {},
async listOAuthTokensByDid() {
return [];
},
async checkAndStoreDpopJti() {
return true;
},
async cleanupExpiredDpopJtis() {},
};
}
function createMockBlobs() {
return {
async put() {},
async get() {
return null;
},
async delete() {},
async has() {
return false;
},
async list() {
return { cids: [], cursor: null };
},
};
}
describe('Read-only mode', () => {
/** @type {PersonalDataServer} */
let pds;
beforeEach(() => {
pds = new PersonalDataServer({
actorStorage: /** @type {any} */ (createMockStorage()),
sharedStorage: /** @type {any} */ (createMockSharedStorage()),
blobs: createMockBlobs(),
jwtSecret: 'test-secret',
readOnly: true,
});
});
it('should allow GET requests (describeServer)', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.server.describeServer',
);
const response = await pds.fetch(request);
expect(response.status).toBe(200);
});
it('should allow GET requests (listRepos)', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.sync.listRepos',
);
const response = await pds.fetch(request);
expect(response.status).toBe(200);
});
it('should reject createSession with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.server.createSession',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: 'test', password: 'pass' }),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
expect(body.message).toBe('This PDS is read-only');
});
it('should reject refreshSession with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.server.refreshSession',
{
method: 'POST',
headers: { Authorization: 'Bearer test-token' },
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject createRecord with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.repo.createRecord',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
repo: 'did:plc:test',
collection: 'app.bsky.feed.post',
record: { text: 'test' },
}),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
expect(body.message).toBe('This PDS is read-only');
});
it('should reject putRecord with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.repo.putRecord',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
repo: 'did:plc:test',
collection: 'app.bsky.feed.post',
rkey: '3abc',
record: { text: 'test' },
}),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject deleteRecord with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.repo.deleteRecord',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
repo: 'did:plc:test',
collection: 'app.bsky.feed.post',
rkey: '3abc',
}),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject applyWrites with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.repo.applyWrites',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
repo: 'did:plc:test',
writes: [],
}),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject uploadBlob with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.repo.uploadBlob',
{
method: 'POST',
headers: {
'Content-Type': 'image/png',
Authorization: 'Bearer test-token',
},
body: new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject putPreferences with 401', async () => {
const request = new Request(
'https://pds.example.com/xrpc/app.bsky.actor.putPreferences',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({ preferences: [] }),
},
);
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject /init with 401', async () => {
const request = new Request('https://pds.example.com/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ did: 'did:plc:test', privateKey: 'abc123' }),
});
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject OAuth PAR with 401', async () => {
const request = new Request('https://pds.example.com/oauth/par', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'client_id=test&redirect_uri=http://localhost',
});
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject OAuth token with 401', async () => {
const request = new Request('https://pds.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'grant_type=authorization_code&code=test',
});
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
it('should reject OAuth revoke with 401', async () => {
const request = new Request('https://pds.example.com/oauth/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'token=test-token',
});
const response = await pds.fetch(request);
expect(response.status).toBe(401);
const body = await response.json();
expect(body.error).toBe('AuthenticationRequired');
});
});
describe('Normal mode (readOnly=false)', () => {
/** @type {PersonalDataServer} */
let pds;
beforeEach(() => {
pds = new PersonalDataServer({
actorStorage: /** @type {any} */ (createMockStorage()),
sharedStorage: /** @type {any} */ (createMockSharedStorage()),
blobs: createMockBlobs(),
jwtSecret: 'test-secret',
readOnly: false,
});
});
it('should allow createSession (fail for other reasons, not read-only)', async () => {
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.server.createSession',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: 'test', password: 'pass' }),
},
);
const response = await pds.fetch(request);
// Won't get read-only error, will get another error because of missing JWT setup
const body = await response.json();
expect(body.message).not.toBe('This PDS is read-only');
});
});
describe('Default mode (readOnly not specified)', () => {
it('should default to non-read-only mode', async () => {
const pds = new PersonalDataServer({
actorStorage: /** @type {any} */ (createMockStorage()),
sharedStorage: /** @type {any} */ (createMockSharedStorage()),
blobs: createMockBlobs(),
jwtSecret: 'test-secret',
// readOnly not specified
});
const request = new Request(
'https://pds.example.com/xrpc/com.atproto.server.createSession',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: 'test', password: 'pass' }),
},
);
const response = await pds.fetch(request);
const body = await response.json();
expect(body.message).not.toBe('This PDS is read-only');
});
});