A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
42
fork

Configure Feed

Select the types of activity you want to include in your feed.

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});