A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
at main 317 lines 11 kB view raw
1// Read-only mode tests 2import { describe, it, expect, beforeEach } from 'vitest'; 3import { PersonalDataServer } from '../packages/core/src/pds.js'; 4 5// Create minimal mock storage 6function createMockStorage() { 7 return { 8 async getRecord() { return null; }, 9 async putRecord() {}, 10 async listRecords() { return { records: [], cursor: null }; }, 11 async listAllRecords() { return []; }, 12 async deleteRecord() {}, 13 async getBlock() { return null; }, 14 async putBlock() {}, 15 async getLatestCommit() { return { seq: 1, cid: 'bafytest', rev: '3abc' }; }, 16 async putCommit() {}, 17 async getEvents() { return { events: [], cursor: 0 }; }, 18 async putEvent() {}, 19 async getBlob() { return null; }, 20 async putBlob() {}, 21 async deleteBlob() {}, 22 async listBlobs() { return { cids: [], cursor: null }; }, 23 async getOrphanedBlobs() { return []; }, 24 async linkBlobToRecord() {}, 25 async unlinkBlobsFromRecord() {}, 26 async getDid() { return 'did:plc:test123'; }, 27 async setDid() {}, 28 async getHandle() { return 'test.handle'; }, 29 async setHandle() {}, 30 async getPrivateKey() { return null; }, 31 async setPrivateKey() {}, 32 async getPreferences() { return []; }, 33 async setPreferences() {}, 34 }; 35} 36 37function createMockSharedStorage() { 38 return { 39 async getActor() { return null; }, 40 async resolveHandle() { return null; }, 41 async putActor() {}, 42 async deleteActor() {}, 43 async getOAuthRequest() { return null; }, 44 async putOAuthRequest() {}, 45 async deleteOAuthRequest() {}, 46 async getOAuthToken() { return null; }, 47 async putOAuthToken() {}, 48 async deleteOAuthToken() {}, 49 async listOAuthTokensByDid() { return []; }, 50 async checkAndStoreDpopJti() { return true; }, 51 async cleanupExpiredDpopJtis() {}, 52 }; 53} 54 55function createMockBlobs() { 56 return { 57 async put() {}, 58 async get() { return null; }, 59 async delete() {}, 60 async has() { return false; }, 61 async list() { return { cids: [], cursor: null }; }, 62 }; 63} 64 65describe('Read-only mode', () => { 66 /** @type {PersonalDataServer} */ 67 let pds; 68 69 beforeEach(() => { 70 pds = new PersonalDataServer({ 71 actorStorage: createMockStorage(), 72 sharedStorage: createMockSharedStorage(), 73 blobs: createMockBlobs(), 74 jwtSecret: 'test-secret', 75 readOnly: true, 76 }); 77 }); 78 79 it('should allow GET requests (describeServer)', async () => { 80 const request = new Request('https://pds.example.com/xrpc/com.atproto.server.describeServer'); 81 const response = await pds.fetch(request); 82 expect(response.status).toBe(200); 83 }); 84 85 it('should allow GET requests (listRepos)', async () => { 86 const request = new Request('https://pds.example.com/xrpc/com.atproto.sync.listRepos'); 87 const response = await pds.fetch(request); 88 expect(response.status).toBe(200); 89 }); 90 91 it('should reject createSession with 401', async () => { 92 const request = new Request('https://pds.example.com/xrpc/com.atproto.server.createSession', { 93 method: 'POST', 94 headers: { 'Content-Type': 'application/json' }, 95 body: JSON.stringify({ identifier: 'test', password: 'pass' }), 96 }); 97 const response = await pds.fetch(request); 98 expect(response.status).toBe(401); 99 const body = await response.json(); 100 expect(body.error).toBe('AuthenticationRequired'); 101 expect(body.message).toBe('This PDS is read-only'); 102 }); 103 104 it('should reject refreshSession with 401', async () => { 105 const request = new Request('https://pds.example.com/xrpc/com.atproto.server.refreshSession', { 106 method: 'POST', 107 headers: { 'Authorization': 'Bearer test-token' }, 108 }); 109 const response = await pds.fetch(request); 110 expect(response.status).toBe(401); 111 const body = await response.json(); 112 expect(body.error).toBe('AuthenticationRequired'); 113 }); 114 115 it('should reject createRecord with 401', async () => { 116 const request = new Request('https://pds.example.com/xrpc/com.atproto.repo.createRecord', { 117 method: 'POST', 118 headers: { 119 'Content-Type': 'application/json', 120 'Authorization': 'Bearer test-token', 121 }, 122 body: JSON.stringify({ 123 repo: 'did:plc:test', 124 collection: 'app.bsky.feed.post', 125 record: { text: 'test' }, 126 }), 127 }); 128 const response = await pds.fetch(request); 129 expect(response.status).toBe(401); 130 const body = await response.json(); 131 expect(body.error).toBe('AuthenticationRequired'); 132 expect(body.message).toBe('This PDS is read-only'); 133 }); 134 135 it('should reject putRecord with 401', async () => { 136 const request = new Request('https://pds.example.com/xrpc/com.atproto.repo.putRecord', { 137 method: 'POST', 138 headers: { 139 'Content-Type': 'application/json', 140 'Authorization': 'Bearer test-token', 141 }, 142 body: JSON.stringify({ 143 repo: 'did:plc:test', 144 collection: 'app.bsky.feed.post', 145 rkey: '3abc', 146 record: { text: 'test' }, 147 }), 148 }); 149 const response = await pds.fetch(request); 150 expect(response.status).toBe(401); 151 const body = await response.json(); 152 expect(body.error).toBe('AuthenticationRequired'); 153 }); 154 155 it('should reject deleteRecord with 401', async () => { 156 const request = new Request('https://pds.example.com/xrpc/com.atproto.repo.deleteRecord', { 157 method: 'POST', 158 headers: { 159 'Content-Type': 'application/json', 160 'Authorization': 'Bearer test-token', 161 }, 162 body: JSON.stringify({ 163 repo: 'did:plc:test', 164 collection: 'app.bsky.feed.post', 165 rkey: '3abc', 166 }), 167 }); 168 const response = await pds.fetch(request); 169 expect(response.status).toBe(401); 170 const body = await response.json(); 171 expect(body.error).toBe('AuthenticationRequired'); 172 }); 173 174 it('should reject applyWrites with 401', async () => { 175 const request = new Request('https://pds.example.com/xrpc/com.atproto.repo.applyWrites', { 176 method: 'POST', 177 headers: { 178 'Content-Type': 'application/json', 179 'Authorization': 'Bearer test-token', 180 }, 181 body: JSON.stringify({ 182 repo: 'did:plc:test', 183 writes: [], 184 }), 185 }); 186 const response = await pds.fetch(request); 187 expect(response.status).toBe(401); 188 const body = await response.json(); 189 expect(body.error).toBe('AuthenticationRequired'); 190 }); 191 192 it('should reject uploadBlob with 401', async () => { 193 const request = new Request('https://pds.example.com/xrpc/com.atproto.repo.uploadBlob', { 194 method: 'POST', 195 headers: { 196 'Content-Type': 'image/png', 197 'Authorization': 'Bearer test-token', 198 }, 199 body: new Uint8Array([0x89, 0x50, 0x4e, 0x47]), 200 }); 201 const response = await pds.fetch(request); 202 expect(response.status).toBe(401); 203 const body = await response.json(); 204 expect(body.error).toBe('AuthenticationRequired'); 205 }); 206 207 it('should reject putPreferences with 401', async () => { 208 const request = new Request('https://pds.example.com/xrpc/app.bsky.actor.putPreferences', { 209 method: 'POST', 210 headers: { 211 'Content-Type': 'application/json', 212 'Authorization': 'Bearer test-token', 213 }, 214 body: JSON.stringify({ preferences: [] }), 215 }); 216 const response = await pds.fetch(request); 217 expect(response.status).toBe(401); 218 const body = await response.json(); 219 expect(body.error).toBe('AuthenticationRequired'); 220 }); 221 222 it('should reject /init with 401', async () => { 223 const request = new Request('https://pds.example.com/init', { 224 method: 'POST', 225 headers: { 'Content-Type': 'application/json' }, 226 body: JSON.stringify({ did: 'did:plc:test', privateKey: 'abc123' }), 227 }); 228 const response = await pds.fetch(request); 229 expect(response.status).toBe(401); 230 const body = await response.json(); 231 expect(body.error).toBe('AuthenticationRequired'); 232 }); 233 234 it('should reject OAuth PAR with 401', async () => { 235 const request = new Request('https://pds.example.com/oauth/par', { 236 method: 'POST', 237 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 238 body: 'client_id=test&redirect_uri=http://localhost', 239 }); 240 const response = await pds.fetch(request); 241 expect(response.status).toBe(401); 242 const body = await response.json(); 243 expect(body.error).toBe('AuthenticationRequired'); 244 }); 245 246 it('should reject OAuth token with 401', async () => { 247 const request = new Request('https://pds.example.com/oauth/token', { 248 method: 'POST', 249 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 250 body: 'grant_type=authorization_code&code=test', 251 }); 252 const response = await pds.fetch(request); 253 expect(response.status).toBe(401); 254 const body = await response.json(); 255 expect(body.error).toBe('AuthenticationRequired'); 256 }); 257 258 it('should reject OAuth revoke with 401', async () => { 259 const request = new Request('https://pds.example.com/oauth/revoke', { 260 method: 'POST', 261 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 262 body: 'token=test-token', 263 }); 264 const response = await pds.fetch(request); 265 expect(response.status).toBe(401); 266 const body = await response.json(); 267 expect(body.error).toBe('AuthenticationRequired'); 268 }); 269}); 270 271describe('Normal mode (readOnly=false)', () => { 272 /** @type {PersonalDataServer} */ 273 let pds; 274 275 beforeEach(() => { 276 pds = new PersonalDataServer({ 277 actorStorage: createMockStorage(), 278 sharedStorage: createMockSharedStorage(), 279 blobs: createMockBlobs(), 280 jwtSecret: 'test-secret', 281 readOnly: false, 282 }); 283 }); 284 285 it('should allow createSession (fail for other reasons, not read-only)', async () => { 286 const request = new Request('https://pds.example.com/xrpc/com.atproto.server.createSession', { 287 method: 'POST', 288 headers: { 'Content-Type': 'application/json' }, 289 body: JSON.stringify({ identifier: 'test', password: 'pass' }), 290 }); 291 const response = await pds.fetch(request); 292 // Won't get read-only error, will get another error because of missing JWT setup 293 const body = await response.json(); 294 expect(body.message).not.toBe('This PDS is read-only'); 295 }); 296}); 297 298describe('Default mode (readOnly not specified)', () => { 299 it('should default to non-read-only mode', async () => { 300 const pds = new PersonalDataServer({ 301 actorStorage: createMockStorage(), 302 sharedStorage: createMockSharedStorage(), 303 blobs: createMockBlobs(), 304 jwtSecret: 'test-secret', 305 // readOnly not specified 306 }); 307 308 const request = new Request('https://pds.example.com/xrpc/com.atproto.server.createSession', { 309 method: 'POST', 310 headers: { 'Content-Type': 'application/json' }, 311 body: JSON.stringify({ identifier: 'test', password: 'pass' }), 312 }); 313 const response = await pds.fetch(request); 314 const body = await response.json(); 315 expect(body.message).not.toBe('This PDS is read-only'); 316 }); 317});