@atproto/bsky 0.0.103 → 0.0.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/api/blob-dispatcher.d.ts +4 -0
- package/dist/api/blob-dispatcher.d.ts.map +1 -0
- package/dist/api/blob-dispatcher.js +37 -0
- package/dist/api/blob-dispatcher.js.map +1 -0
- package/dist/api/blob-resolver.d.ts +17 -8
- package/dist/api/blob-resolver.d.ts.map +1 -1
- package/dist/api/blob-resolver.js +246 -99
- package/dist/api/blob-resolver.js.map +1 -1
- package/dist/api/well-known.d.ts.map +1 -1
- package/dist/api/well-known.js +30 -24
- package/dist/api/well-known.js.map +1 -1
- package/dist/config.d.ts +14 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +40 -5
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +3 -0
- package/dist/context.js.map +1 -1
- package/dist/data-plane/server/indexing/index.js +2 -2
- package/dist/image/server.d.ts +7 -13
- package/dist/image/server.d.ts.map +1 -1
- package/dist/image/server.js +119 -115
- package/dist/image/server.js.map +1 -1
- package/dist/image/sharp.d.ts +11 -2
- package/dist/image/sharp.d.ts.map +1 -1
- package/dist/image/sharp.js +35 -38
- package/dist/image/sharp.js.map +1 -1
- package/dist/image/util.d.ts +6 -4
- package/dist/image/util.d.ts.map +1 -1
- package/dist/image/util.js +14 -10
- package/dist/image/util.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -10
- package/dist/index.js.map +1 -1
- package/dist/util/http.d.ts +12 -0
- package/dist/util/http.d.ts.map +1 -0
- package/dist/util/http.js +36 -0
- package/dist/util/http.js.map +1 -0
- package/dist/util/retry.d.ts +2 -5
- package/dist/util/retry.d.ts.map +1 -1
- package/dist/util/retry.js +8 -27
- package/dist/util/retry.js.map +1 -1
- package/package.json +17 -13
- package/src/api/blob-dispatcher.ts +38 -0
- package/src/api/blob-resolver.ts +341 -106
- package/src/api/well-known.ts +31 -24
- package/src/config.ts +63 -6
- package/src/context.ts +6 -0
- package/src/data-plane/server/indexing/index.ts +3 -3
- package/src/image/server.ts +131 -107
- package/src/image/sharp.ts +48 -52
- package/src/image/util.ts +20 -12
- package/src/index.ts +8 -15
- package/src/util/http.ts +41 -0
- package/src/util/retry.ts +8 -32
- package/tests/_util.ts +50 -3
- package/tests/blob-resolver.test.ts +62 -36
- package/tests/image/server.test.ts +40 -32
- package/tests/image/sharp.test.ts +17 -4
- package/tests/label-hydration.test.ts +6 -6
- package/tests/server.test.ts +41 -56
- package/tsconfig.build.tsbuildinfo +1 -1
package/tests/_util.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AppBskyFeedGetPostThread } from '@atproto/api'
|
|
2
2
|
import { lexToJson } from '@atproto/lexicon'
|
|
3
|
+
import { AtUri } from '@atproto/syntax'
|
|
4
|
+
import { type Express } from 'express'
|
|
3
5
|
import { CID } from 'multiformats/cid'
|
|
6
|
+
import { Server } from 'node:http'
|
|
7
|
+
|
|
8
|
+
import { AddressInfo } from 'node:net'
|
|
9
|
+
import { isViewRecord } from '../src/lexicon/types/app/bsky/embed/record'
|
|
4
10
|
import {
|
|
5
11
|
FeedViewPost,
|
|
6
12
|
PostView,
|
|
7
13
|
isPostView,
|
|
8
14
|
isThreadViewPost,
|
|
9
15
|
} from '../src/lexicon/types/app/bsky/feed/defs'
|
|
10
|
-
import { isViewRecord } from '../src/lexicon/types/app/bsky/embed/record'
|
|
11
|
-
import { AppBskyFeedGetPostThread } from '@atproto/api'
|
|
12
16
|
import {
|
|
13
17
|
LabelerView,
|
|
14
18
|
isLabelerView,
|
|
@@ -234,3 +238,46 @@ export const stripViewerFromLabeler = (
|
|
|
234
238
|
labeler.creator = stripViewer(labeler.creator)
|
|
235
239
|
return stripViewer(labeler)
|
|
236
240
|
}
|
|
241
|
+
|
|
242
|
+
export async function startServer(app: Express) {
|
|
243
|
+
return new Promise<{
|
|
244
|
+
origin: string
|
|
245
|
+
server: Server
|
|
246
|
+
stop: () => Promise<void>
|
|
247
|
+
}>((resolve, reject) => {
|
|
248
|
+
const onListen = () => {
|
|
249
|
+
const port = (server.address() as AddressInfo).port
|
|
250
|
+
resolve({
|
|
251
|
+
server,
|
|
252
|
+
origin: `http://localhost:${port}`,
|
|
253
|
+
stop: () => stopServer(server),
|
|
254
|
+
})
|
|
255
|
+
cleanup()
|
|
256
|
+
}
|
|
257
|
+
const onError = (err: Error) => {
|
|
258
|
+
reject(err)
|
|
259
|
+
cleanup()
|
|
260
|
+
}
|
|
261
|
+
const cleanup = () => {
|
|
262
|
+
server.removeListener('listening', onListen)
|
|
263
|
+
server.removeListener('error', onError)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const server = app
|
|
267
|
+
.listen(0)
|
|
268
|
+
.once('listening', onListen)
|
|
269
|
+
.once('error', onError)
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function stopServer(server: Server) {
|
|
274
|
+
return new Promise<void>((resolve, reject) => {
|
|
275
|
+
server.close((err) => {
|
|
276
|
+
if (err) {
|
|
277
|
+
reject(err)
|
|
278
|
+
} else {
|
|
279
|
+
resolve()
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import axios, { AxiosInstance } from 'axios'
|
|
2
|
-
import { CID } from 'multiformats/cid'
|
|
3
1
|
import { cidForCbor, verifyCidForBytes } from '@atproto/common'
|
|
4
|
-
import { TestNetwork, basicSeed } from '@atproto/dev-env'
|
|
5
2
|
import { randomBytes } from '@atproto/crypto'
|
|
3
|
+
import { TestNetwork, basicSeed } from '@atproto/dev-env'
|
|
4
|
+
import { CID } from 'multiformats/cid'
|
|
5
|
+
import { request } from 'undici'
|
|
6
6
|
|
|
7
7
|
describe('blob resolver', () => {
|
|
8
8
|
let network: TestNetwork
|
|
9
|
-
let client: AxiosInstance
|
|
10
9
|
let fileDid: string
|
|
11
10
|
let fileCid: CID
|
|
11
|
+
let fileSize: number
|
|
12
12
|
|
|
13
13
|
beforeAll(async () => {
|
|
14
14
|
network = await TestNetwork.create({
|
|
@@ -19,10 +19,7 @@ describe('blob resolver', () => {
|
|
|
19
19
|
await network.processAll()
|
|
20
20
|
fileDid = sc.dids.carol
|
|
21
21
|
fileCid = sc.posts[fileDid][0].images[0].image.ref
|
|
22
|
-
|
|
23
|
-
baseURL: network.bsky.url,
|
|
24
|
-
validateStatus: () => true,
|
|
25
|
-
})
|
|
22
|
+
fileSize = sc.posts[fileDid][0].images[0].image.size
|
|
26
23
|
})
|
|
27
24
|
|
|
28
25
|
afterAll(async () => {
|
|
@@ -30,70 +27,99 @@ describe('blob resolver', () => {
|
|
|
30
27
|
})
|
|
31
28
|
|
|
32
29
|
it('resolves blob with good signature check.', async () => {
|
|
33
|
-
const
|
|
34
|
-
`/blob/${fileDid}/${fileCid.toString()}`,
|
|
35
|
-
{ responseType: 'arraybuffer' },
|
|
30
|
+
const response = await request(
|
|
31
|
+
new URL(`/blob/${fileDid}/${fileCid.toString()}`, network.bsky.url),
|
|
36
32
|
)
|
|
37
|
-
expect(
|
|
38
|
-
expect(headers['content-type']).toEqual('image/jpeg')
|
|
39
|
-
expect(headers['content-security-policy']).toEqual(
|
|
33
|
+
expect(response.statusCode).toEqual(200)
|
|
34
|
+
expect(response.headers['content-type']).toEqual('image/jpeg')
|
|
35
|
+
expect(response.headers['content-security-policy']).toEqual(
|
|
40
36
|
`default-src 'none'; sandbox`,
|
|
41
37
|
)
|
|
42
|
-
expect(headers['x-content-type-options']).toEqual('nosniff')
|
|
43
|
-
|
|
38
|
+
expect(response.headers['x-content-type-options']).toEqual('nosniff')
|
|
39
|
+
|
|
40
|
+
const bytes = new Uint8Array(await response.body.arrayBuffer())
|
|
41
|
+
await expect(verifyCidForBytes(fileCid, bytes)).resolves.toBeUndefined()
|
|
44
42
|
})
|
|
45
43
|
|
|
46
44
|
it('404s on missing blob.', async () => {
|
|
47
45
|
const badCid = await cidForCbor({ unknown: true })
|
|
48
|
-
const
|
|
49
|
-
`/blob/${fileDid}/${badCid.toString()}`,
|
|
46
|
+
const response = await request(
|
|
47
|
+
new URL(`/blob/${fileDid}/${badCid.toString()}`, network.bsky.url),
|
|
50
48
|
)
|
|
51
|
-
expect(
|
|
52
|
-
expect(
|
|
49
|
+
expect(response.statusCode).toEqual(404)
|
|
50
|
+
await expect(response.body.json()).resolves.toEqual({
|
|
53
51
|
error: 'NotFoundError',
|
|
54
52
|
message: 'Blob not found',
|
|
55
53
|
})
|
|
56
54
|
})
|
|
57
55
|
|
|
58
56
|
it('404s on missing identity.', async () => {
|
|
59
|
-
const
|
|
60
|
-
|
|
57
|
+
const nonExistingDid = `did:plc:${'a'.repeat(24)}`
|
|
58
|
+
|
|
59
|
+
const response = await request(
|
|
60
|
+
new URL(
|
|
61
|
+
`/blob/${nonExistingDid}/${fileCid.toString()}`,
|
|
62
|
+
network.bsky.url,
|
|
63
|
+
),
|
|
61
64
|
)
|
|
62
|
-
expect(
|
|
63
|
-
expect(
|
|
65
|
+
expect(response.statusCode).toEqual(404)
|
|
66
|
+
await expect(response.body.json()).resolves.toEqual({
|
|
64
67
|
error: 'NotFoundError',
|
|
65
68
|
message: 'Origin not found',
|
|
66
69
|
})
|
|
67
70
|
})
|
|
68
71
|
|
|
69
72
|
it('400s on invalid did.', async () => {
|
|
70
|
-
const
|
|
71
|
-
`/blob/did::/${fileCid.toString()}`,
|
|
73
|
+
const response = await request(
|
|
74
|
+
new URL(`/blob/did::/${fileCid.toString()}`, network.bsky.url),
|
|
72
75
|
)
|
|
73
|
-
expect(
|
|
74
|
-
expect(
|
|
76
|
+
expect(response.statusCode).toEqual(400)
|
|
77
|
+
await expect(response.body.json()).resolves.toEqual({
|
|
75
78
|
error: 'BadRequestError',
|
|
76
79
|
message: 'Invalid did',
|
|
77
80
|
})
|
|
78
81
|
})
|
|
79
82
|
|
|
80
83
|
it('400s on invalid cid.', async () => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const response = await request(
|
|
85
|
+
new URL(`/blob/${fileDid}/barfy`, network.bsky.url),
|
|
86
|
+
)
|
|
87
|
+
expect(response.statusCode).toEqual(400)
|
|
88
|
+
await expect(response.body.json()).resolves.toEqual({
|
|
84
89
|
error: 'BadRequestError',
|
|
85
90
|
message: 'Invalid cid',
|
|
86
91
|
})
|
|
87
92
|
})
|
|
88
93
|
|
|
89
|
-
it('
|
|
94
|
+
it('400s on missing file.', async () => {
|
|
95
|
+
const missingCid = await cidForCbor('missing-file')
|
|
96
|
+
|
|
97
|
+
const response = await request(
|
|
98
|
+
new URL(`/blob/${fileDid}/${missingCid}`, network.bsky.url),
|
|
99
|
+
)
|
|
100
|
+
expect(response.statusCode).toEqual(404)
|
|
101
|
+
await expect(response.body.json()).resolves.toEqual({
|
|
102
|
+
error: 'NotFoundError',
|
|
103
|
+
message: 'Blob not found',
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('replaces the file with invalid bytes.', async () => {
|
|
90
108
|
await network.pds.ctx.blobstore(fileDid).delete(fileCid)
|
|
91
109
|
await network.pds.ctx
|
|
92
110
|
.blobstore(fileDid)
|
|
93
|
-
.putPermanent(fileCid, randomBytes(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
.putPermanent(fileCid, randomBytes(fileSize))
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('fails to fetch bytes on blob with bad signature check.', async () => {
|
|
115
|
+
const response = await request(
|
|
116
|
+
new URL(`/blob/${fileDid}/${fileCid.toString()}`, network.bsky.url),
|
|
97
117
|
)
|
|
118
|
+
|
|
119
|
+
expect(response.statusCode).toEqual(404)
|
|
120
|
+
await expect(response.body.json()).resolves.toEqual({
|
|
121
|
+
error: 'NotFoundError',
|
|
122
|
+
message: 'Bad cid check',
|
|
123
|
+
})
|
|
98
124
|
})
|
|
99
125
|
})
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import axios, { AxiosInstance } from 'axios'
|
|
2
|
-
import { CID } from 'multiformats/cid'
|
|
3
1
|
import { cidForCbor } from '@atproto/common'
|
|
4
2
|
import { TestNetwork, basicSeed } from '@atproto/dev-env'
|
|
3
|
+
import { CID } from 'multiformats/cid'
|
|
4
|
+
import { Readable } from 'node:stream'
|
|
5
5
|
import { getInfo } from '../../src/image/sharp'
|
|
6
6
|
import { ImageUriBuilder } from '../../src/image/uri'
|
|
7
7
|
|
|
8
8
|
describe('image processing server', () => {
|
|
9
9
|
let network: TestNetwork
|
|
10
|
-
let client: AxiosInstance
|
|
11
10
|
let fileDid: string
|
|
12
11
|
let fileCid: CID
|
|
13
12
|
|
|
@@ -20,10 +19,6 @@ describe('image processing server', () => {
|
|
|
20
19
|
await network.processAll()
|
|
21
20
|
fileDid = sc.dids.carol
|
|
22
21
|
fileCid = sc.posts[fileDid][0].images[0].image.ref
|
|
23
|
-
client = axios.create({
|
|
24
|
-
baseURL: `${network.bsky.url}/img`,
|
|
25
|
-
validateStatus: () => true,
|
|
26
|
-
})
|
|
27
22
|
})
|
|
28
23
|
|
|
29
24
|
afterAll(async () => {
|
|
@@ -31,16 +26,19 @@ describe('image processing server', () => {
|
|
|
31
26
|
})
|
|
32
27
|
|
|
33
28
|
it('processes image from blob resolver.', async () => {
|
|
34
|
-
const res = await
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
const res = await fetch(
|
|
30
|
+
new URL(
|
|
31
|
+
`/img${ImageUriBuilder.getPath({
|
|
32
|
+
preset: 'feed_fullsize',
|
|
33
|
+
did: fileDid,
|
|
34
|
+
cid: fileCid.toString(),
|
|
35
|
+
})}`,
|
|
36
|
+
network.bsky.url,
|
|
37
|
+
),
|
|
41
38
|
)
|
|
42
39
|
|
|
43
|
-
const
|
|
40
|
+
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
41
|
+
const info = await getInfo(Readable.from([bytes]))
|
|
44
42
|
|
|
45
43
|
expect(info).toEqual({
|
|
46
44
|
height: 580,
|
|
@@ -48,7 +46,7 @@ describe('image processing server', () => {
|
|
|
48
46
|
size: 127578,
|
|
49
47
|
mime: 'image/jpeg',
|
|
50
48
|
})
|
|
51
|
-
expect(res.headers).toEqual(
|
|
49
|
+
expect(Object.fromEntries(res.headers)).toEqual(
|
|
52
50
|
expect.objectContaining({
|
|
53
51
|
'content-type': 'image/jpeg',
|
|
54
52
|
'cache-control': 'public, max-age=31536000',
|
|
@@ -63,26 +61,36 @@ describe('image processing server', () => {
|
|
|
63
61
|
did: fileDid,
|
|
64
62
|
cid: fileCid.toString(),
|
|
65
63
|
})
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
expect(
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
expect(
|
|
73
|
-
|
|
64
|
+
const url = new URL(`/img${path}`, network.bsky.url)
|
|
65
|
+
|
|
66
|
+
const res1 = await fetch(url)
|
|
67
|
+
expect(res1.headers.get('x-cache')).toEqual('miss')
|
|
68
|
+
const bytes1 = new Uint8Array(await res1.arrayBuffer())
|
|
69
|
+
const res2 = await fetch(url)
|
|
70
|
+
expect(res2.headers.get('x-cache')).toEqual('hit')
|
|
71
|
+
const bytes2 = new Uint8Array(await res2.arrayBuffer())
|
|
72
|
+
const res3 = await fetch(url)
|
|
73
|
+
expect(res3.headers.get('x-cache')).toEqual('hit')
|
|
74
|
+
const bytes3 = new Uint8Array(await res3.arrayBuffer())
|
|
75
|
+
expect(Buffer.compare(bytes1, bytes2)).toEqual(0)
|
|
76
|
+
expect(Buffer.compare(bytes1, bytes3)).toEqual(0)
|
|
74
77
|
})
|
|
75
78
|
|
|
76
79
|
it('errors on missing file.', async () => {
|
|
77
80
|
const missingCid = await cidForCbor('missing-file')
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
|
|
82
|
+
const path = ImageUriBuilder.getPath({
|
|
83
|
+
preset: 'feed_fullsize',
|
|
84
|
+
did: fileDid,
|
|
85
|
+
cid: missingCid.toString(),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const url = new URL(`/img${path}`, network.bsky.url)
|
|
89
|
+
|
|
90
|
+
const res = await fetch(url)
|
|
85
91
|
expect(res.status).toEqual(404)
|
|
86
|
-
expect(res.
|
|
92
|
+
await expect(res.json()).resolves.toMatchObject({
|
|
93
|
+
message: 'Blob not found',
|
|
94
|
+
})
|
|
87
95
|
})
|
|
88
96
|
})
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { createReadStream } from 'fs'
|
|
2
|
-
import {
|
|
1
|
+
import { createReadStream } from 'node:fs'
|
|
2
|
+
import { pipeline } from 'node:stream/promises'
|
|
3
|
+
import {
|
|
4
|
+
Options,
|
|
5
|
+
createImageProcessor,
|
|
6
|
+
createImageUpscaler,
|
|
7
|
+
getInfo,
|
|
8
|
+
} from '../../src/image/sharp'
|
|
3
9
|
|
|
4
10
|
describe('sharp image processor', () => {
|
|
5
11
|
it('scales up to cover.', async () => {
|
|
@@ -179,7 +185,14 @@ describe('sharp image processor', () => {
|
|
|
179
185
|
|
|
180
186
|
async function processFixture(fixture: string, options: Options) {
|
|
181
187
|
const image = createReadStream(`../dev-env/assets/${fixture}`)
|
|
182
|
-
const
|
|
183
|
-
|
|
188
|
+
const upscaler = createImageUpscaler(options)
|
|
189
|
+
const processor = createImageProcessor(options)
|
|
190
|
+
|
|
191
|
+
const [info] = await Promise.all([
|
|
192
|
+
getInfo(processor),
|
|
193
|
+
pipeline([image, upscaler, processor]),
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
return info
|
|
184
197
|
}
|
|
185
198
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AtpAgent } from '@atproto/api'
|
|
2
2
|
import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
|
|
3
|
-
import axios from 'axios'
|
|
4
3
|
|
|
5
4
|
describe('label hydration', () => {
|
|
6
5
|
let network: TestNetwork
|
|
@@ -81,15 +80,16 @@ describe('label hydration', () => {
|
|
|
81
80
|
})
|
|
82
81
|
|
|
83
82
|
it('defaults to service labels when no labeler header is provided', async () => {
|
|
84
|
-
const res = await
|
|
83
|
+
const res = await fetch(
|
|
85
84
|
`${network.pds.url}/xrpc/app.bsky.actor.getProfile?actor=${carol}`,
|
|
86
85
|
{ headers: sc.getHeaders(bob) },
|
|
87
86
|
)
|
|
88
|
-
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
87
|
+
const data = await res.json()
|
|
88
|
+
expect(data.labels?.length).toBe(1)
|
|
89
|
+
expect(data.labels?.[0].src).toBe(labelerDid)
|
|
90
|
+
expect(data.labels?.[0].val).toBe('misleading')
|
|
91
91
|
|
|
92
|
-
expect(res.headers
|
|
92
|
+
expect(res.headers.get('atproto-content-labelers')).toEqual(
|
|
93
93
|
network.bsky.ctx.cfg.labelsFromIssuerDids
|
|
94
94
|
.map((did) => `${did};redact`)
|
|
95
95
|
.join(','),
|
package/tests/server.test.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { AddressInfo } from 'net'
|
|
2
|
-
import express from 'express'
|
|
3
|
-
import axios, { AxiosError } from 'axios'
|
|
4
1
|
import { TestNetwork, basicSeed } from '@atproto/dev-env'
|
|
2
|
+
import express from 'express'
|
|
3
|
+
import { once } from 'node:events'
|
|
4
|
+
import { AddressInfo } from 'node:net'
|
|
5
|
+
import { finished } from 'node:stream/promises'
|
|
6
|
+
import { request } from 'undici'
|
|
5
7
|
import { handler as errorHandler } from '../src/error'
|
|
6
|
-
import {
|
|
8
|
+
import { startServer } from './_util'
|
|
7
9
|
|
|
8
10
|
describe('server', () => {
|
|
9
11
|
let network: TestNetwork
|
|
@@ -24,8 +26,8 @@ describe('server', () => {
|
|
|
24
26
|
})
|
|
25
27
|
|
|
26
28
|
it('preserves 404s.', async () => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
+
const response = await fetch(`${network.bsky.url}/unknown`)
|
|
30
|
+
expect(response.status).toEqual(404)
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
it('error handler turns unknown errors into 500s.', async () => {
|
|
@@ -34,99 +36,82 @@ describe('server', () => {
|
|
|
34
36
|
throw new Error('Oops!')
|
|
35
37
|
})
|
|
36
38
|
app.use(errorHandler)
|
|
37
|
-
const
|
|
38
|
-
const port = (srv.address() as AddressInfo).port
|
|
39
|
-
const promise = axios.get(`http://localhost:${port}/oops`)
|
|
40
|
-
await expect(promise).rejects.toThrow('failed with status code 500')
|
|
41
|
-
srv.close()
|
|
39
|
+
const { origin, stop } = await startServer(app)
|
|
42
40
|
try {
|
|
43
|
-
await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(axiosError.response?.status).toEqual(500)
|
|
47
|
-
expect(axiosError.response?.data).toEqual({
|
|
41
|
+
const response = await fetch(new URL(`/oops`, origin))
|
|
42
|
+
expect(response.status).toEqual(500)
|
|
43
|
+
await expect(response.json()).resolves.toEqual({
|
|
48
44
|
error: 'InternalServerError',
|
|
49
45
|
message: 'Internal Server Error',
|
|
50
46
|
})
|
|
47
|
+
} finally {
|
|
48
|
+
await stop()
|
|
51
49
|
}
|
|
52
50
|
})
|
|
53
51
|
|
|
54
52
|
it('healthcheck succeeds when database is available.', async () => {
|
|
55
|
-
const
|
|
56
|
-
expect(status).toEqual(200)
|
|
57
|
-
expect(
|
|
53
|
+
const response = await fetch(`${network.bsky.url}/xrpc/_health`)
|
|
54
|
+
expect(response.status).toEqual(200)
|
|
55
|
+
await expect(response.json()).resolves.toEqual({ version: 'unknown' })
|
|
58
56
|
})
|
|
59
57
|
|
|
60
58
|
// TODO(bsky) check on a different endpoint that accepts json, currently none.
|
|
61
59
|
it.skip('limits size of json input.', async () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw new Error('Request should have failed')
|
|
72
|
-
} catch (err) {
|
|
73
|
-
if (axios.isAxiosError(err)) {
|
|
74
|
-
error = err
|
|
75
|
-
} else {
|
|
76
|
-
throw err
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
expect(error.response?.status).toEqual(413)
|
|
80
|
-
expect(error.response?.data).toEqual({
|
|
60
|
+
const response = await fetch(
|
|
61
|
+
`${network.bsky.url}/xrpc/com.atproto.repo.createRecord`,
|
|
62
|
+
{
|
|
63
|
+
body: 'x'.repeat(100 * 1024), // 100kb
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(response.status).toEqual(413)
|
|
68
|
+
await expect(response.json()).resolves.toEqual({
|
|
81
69
|
error: 'PayloadTooLargeError',
|
|
82
70
|
message: 'request entity too large',
|
|
83
71
|
})
|
|
84
72
|
})
|
|
85
73
|
|
|
86
74
|
it('compresses large json responses', async () => {
|
|
87
|
-
const res = await
|
|
75
|
+
const res = await request(
|
|
88
76
|
`${network.bsky.url}/xrpc/app.bsky.feed.getTimeline`,
|
|
89
77
|
{
|
|
90
|
-
decompress: false,
|
|
91
78
|
headers: {
|
|
92
79
|
...(await network.serviceHeaders(alice, 'app.bsky.feed.getTimeline')),
|
|
93
80
|
'accept-encoding': 'gzip',
|
|
94
81
|
},
|
|
95
82
|
},
|
|
96
83
|
)
|
|
84
|
+
|
|
85
|
+
await finished(res.body.resume())
|
|
86
|
+
|
|
97
87
|
expect(res.headers['content-encoding']).toEqual('gzip')
|
|
98
88
|
})
|
|
99
89
|
|
|
100
90
|
it('does not compress small payloads', async () => {
|
|
101
|
-
const res = await
|
|
102
|
-
decompress: false,
|
|
91
|
+
const res = await request(`${network.bsky.url}/xrpc/_health`, {
|
|
103
92
|
headers: { 'accept-encoding': 'gzip' },
|
|
104
93
|
})
|
|
94
|
+
|
|
95
|
+
await finished(res.body.resume())
|
|
96
|
+
|
|
105
97
|
expect(res.headers['content-encoding']).toBeUndefined()
|
|
106
98
|
})
|
|
107
99
|
|
|
108
100
|
it('healthcheck fails when dataplane is unavailable.', async () => {
|
|
109
101
|
const { port } = network.bsky.dataplane.server.address() as AddressInfo
|
|
110
102
|
await network.bsky.dataplane.destroy()
|
|
111
|
-
|
|
103
|
+
|
|
112
104
|
try {
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
error
|
|
118
|
-
}
|
|
119
|
-
throw err
|
|
120
|
-
}
|
|
105
|
+
const response = await fetch(`${network.bsky.url}/xrpc/_health`)
|
|
106
|
+
expect(response.status).toEqual(503)
|
|
107
|
+
await expect(response.json()).resolves.toEqual({
|
|
108
|
+
version: 'unknown',
|
|
109
|
+
error: 'Service Unavailable',
|
|
110
|
+
})
|
|
121
111
|
} finally {
|
|
122
112
|
// restart dataplane server to allow test suite to cleanup
|
|
123
113
|
network.bsky.dataplane.server.listen(port)
|
|
124
114
|
await once(network.bsky.dataplane.server, 'listening')
|
|
125
115
|
}
|
|
126
|
-
expect(error.response?.status).toEqual(503)
|
|
127
|
-
expect(error.response?.data).toEqual({
|
|
128
|
-
version: 'unknown',
|
|
129
|
-
error: 'Service Unavailable',
|
|
130
|
-
})
|
|
131
116
|
})
|
|
132
117
|
})
|