c62651dd69
* Allow validation of legacy blobs (in 'validate' mode) * tidy * tidy * tidy * changeset * tidy * review comments * update skills * tidy * Add `TypedBlobRef` * fix style * review comments * lint
810 lines
24 KiB
TypeScript
810 lines
24 KiB
TypeScript
import { assert, describe, expect, expectTypeOf, it, vi } from 'vitest'
|
|
import { LexValue, cidForLex } from '@atproto/lex-cbor'
|
|
import { cidForRawBytes, isTypedBlobRef, parseCid } from '@atproto/lex-data'
|
|
import { lexParse, lexToJson } from '@atproto/lex-json'
|
|
import {
|
|
$Typed,
|
|
LexValidationError,
|
|
toDatetimeString,
|
|
} from '@atproto/lex-schema'
|
|
import {
|
|
Action,
|
|
Client,
|
|
FetchHandler,
|
|
XrpcAuthenticationError,
|
|
XrpcInvalidResponseError,
|
|
XrpcResponseError,
|
|
} from '../src/index.js'
|
|
import { app, com } from './lexicons/index.js'
|
|
|
|
const cborCid = parseCid(
|
|
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
{ flavor: 'cbor' },
|
|
)
|
|
|
|
type Preference = app.bsky.actor.defs.Preferences[number]
|
|
|
|
describe('utils', () => {
|
|
describe('TypedObjectSchema', () => {
|
|
describe('build()', () => {
|
|
it('overrides $type when building an object', () => {
|
|
function expectAdultContentPref(
|
|
_: app.bsky.actor.defs.AdultContentPref,
|
|
) {}
|
|
function expectTypedAdultContentPref(
|
|
_: $Typed<app.bsky.actor.defs.AdultContentPref>,
|
|
) {}
|
|
|
|
const pref = app.bsky.actor.defs.adultContentPref.build({
|
|
// @ts-expect-error
|
|
$type: 'foo',
|
|
enabled: true,
|
|
})
|
|
|
|
expectAdultContentPref(pref)
|
|
expectTypedAdultContentPref(pref)
|
|
|
|
expect(pref).toStrictEqual({
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: true,
|
|
})
|
|
|
|
expectTypeOf(pref).toEqualTypeOf<{
|
|
$type: 'app.bsky.actor.defs#adultContentPref'
|
|
enabled: boolean
|
|
}>()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Client', () => {
|
|
describe('actions', () => {
|
|
it('updatePreferences', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {
|
|
if (url === '/xrpc/app.bsky.actor.getPreferences') {
|
|
expect(init?.method).toBe('GET')
|
|
expect(init?.body).toBeUndefined()
|
|
return Response.json({ preferences: storedPreferences })
|
|
} else if (url === '/xrpc/app.bsky.actor.putPreferences') {
|
|
expect(init?.method).toBe('POST')
|
|
expect(typeof init?.body).toBe('string')
|
|
const result = app.bsky.actor.putPreferences.$input.schema.safeParse(
|
|
lexParse(init?.body as string),
|
|
)
|
|
if (result.success) {
|
|
storedPreferences = result.value.preferences
|
|
return new Response(null, { status: 204 })
|
|
} else {
|
|
return Response.json(
|
|
{ error: 'InvalidRequest', message: result.reason.message },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
} else {
|
|
return new Response('Not Found', { status: 404 })
|
|
}
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
const updatePreferences: Action<
|
|
(pref: Preference[]) => false | Preference[],
|
|
Preference[]
|
|
> = async function (client, updatePreferences, options) {
|
|
const data = await client.call(
|
|
app.bsky.actor.getPreferences,
|
|
{},
|
|
options,
|
|
)
|
|
|
|
const preferences = updatePreferences(data.preferences)
|
|
if (preferences === false) return data.preferences
|
|
|
|
options?.signal?.throwIfAborted()
|
|
|
|
await client.call(
|
|
app.bsky.actor.putPreferences,
|
|
{ preferences },
|
|
options,
|
|
)
|
|
|
|
return preferences
|
|
}
|
|
|
|
const upsertPreference: Action<Preference, Preference[]> =
|
|
async function (client, pref, options) {
|
|
return updatePreferences(
|
|
client,
|
|
(prefs) => [...prefs.filter((p) => p.$type !== pref.$type), pref],
|
|
options,
|
|
)
|
|
}
|
|
|
|
let storedPreferences: Preference[] = [
|
|
app.bsky.actor.defs.adultContentPref.build({
|
|
enabled: false,
|
|
}),
|
|
app.bsky.actor.defs.contentLabelPref.build({
|
|
label: 'my-label',
|
|
visibility: 'warn',
|
|
}),
|
|
]
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(0)
|
|
expect(storedPreferences).toEqual([
|
|
{
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: false,
|
|
},
|
|
{
|
|
$type: 'app.bsky.actor.defs#contentLabelPref',
|
|
label: 'my-label',
|
|
visibility: 'warn',
|
|
},
|
|
])
|
|
|
|
// Upsert adult content preference
|
|
await client.call(
|
|
upsertPreference,
|
|
app.bsky.actor.defs.adultContentPref.build({
|
|
enabled: true,
|
|
}),
|
|
)
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(2)
|
|
expect(storedPreferences).toEqual([
|
|
{
|
|
$type: 'app.bsky.actor.defs#contentLabelPref',
|
|
label: 'my-label',
|
|
visibility: 'warn',
|
|
},
|
|
{
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: true,
|
|
},
|
|
])
|
|
|
|
// @ts-expect-error invalid preference value
|
|
await client.call(upsertPreference, {
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
// enabled: true,
|
|
})
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(4)
|
|
expect(storedPreferences).toEqual([
|
|
{
|
|
$type: 'app.bsky.actor.defs#contentLabelPref',
|
|
label: 'my-label',
|
|
visibility: 'warn',
|
|
},
|
|
{
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: false, // "false" default will be enforced when parsing the body
|
|
},
|
|
])
|
|
|
|
await expect(async () => {
|
|
await client.call(upsertPreference, {
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
// @ts-expect-error invalid preference value
|
|
enabled: 'not-a-boolean',
|
|
})
|
|
}).rejects.toThrow('Expected boolean value')
|
|
})
|
|
})
|
|
|
|
describe('query', () => {
|
|
it('allows perfoming a GET request and parsing the response', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {
|
|
expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')
|
|
expect(init?.method).toBe('GET')
|
|
expect(init?.body).toBeUndefined()
|
|
|
|
return Response.json({
|
|
preferences: [
|
|
{
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: false,
|
|
},
|
|
{
|
|
$type: 'app.bsky.actor.defs#someOtherPref',
|
|
otherField: 'some value',
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
const { preferences } = await client.call(app.bsky.actor.getPreferences)
|
|
|
|
expect(preferences).toEqual([
|
|
{
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: false,
|
|
},
|
|
{
|
|
$type: 'app.bsky.actor.defs#someOtherPref',
|
|
otherField: 'some value',
|
|
},
|
|
])
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(1)
|
|
|
|
const adultContentPref = preferences.find((p) =>
|
|
app.bsky.actor.defs.adultContentPref.isTypeOf(p),
|
|
)
|
|
|
|
expect(adultContentPref).toEqual({
|
|
$type: 'app.bsky.actor.defs#adultContentPref',
|
|
enabled: false,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('errors', () => {
|
|
it('handles invalid XRPC error payloads', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json(
|
|
{ invalidField: 'this is not a valid xrpc error payload' },
|
|
{ status: 400 },
|
|
)
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
await expect(
|
|
client.call(app.bsky.actor.getPreferences),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof XrpcResponseError)
|
|
expect(err.message).toMatch(
|
|
'Upstream server responded with a 400 error',
|
|
)
|
|
return true
|
|
})
|
|
})
|
|
|
|
it('uses the status code to construct the "error"', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return new Response(null, { status: 429 })
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
await expect(
|
|
client.call(app.bsky.actor.getPreferences),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof XrpcResponseError)
|
|
expect(err.error).toBe('RateLimitExceeded')
|
|
expect(err.message).toBe('Upstream server responded with a 429 error')
|
|
return true
|
|
})
|
|
})
|
|
|
|
it('handles XRPC errors with invalid status code', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return new Response(null, { status: 302 })
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
await expect(
|
|
client.call(app.bsky.actor.getPreferences),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof XrpcInvalidResponseError)
|
|
expect(err.message).toMatch('Unexpected status code 302')
|
|
return true
|
|
})
|
|
})
|
|
|
|
it('handles XRPC server errors', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return new Response('<p>Server error</p>', {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'text/html' },
|
|
})
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
await expect(
|
|
client.call(app.bsky.actor.getPreferences),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof XrpcResponseError)
|
|
expect(err.error).toBe('InternalServerError')
|
|
expect(err.message).toBe('Upstream server responded with a 500 error')
|
|
expect(err.payload).toEqual({
|
|
encoding: 'text/html',
|
|
body: new Uint8Array(Buffer.from('<p>Server error</p>')),
|
|
})
|
|
return true
|
|
})
|
|
})
|
|
|
|
it('propagates server error messages', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json(
|
|
{
|
|
error: 'CustomError',
|
|
message: 'This is a custom error message from the server',
|
|
},
|
|
{
|
|
status: 400,
|
|
},
|
|
)
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
await expect(
|
|
client.call(app.bsky.actor.getPreferences),
|
|
).rejects.toMatchObject({
|
|
name: 'XrpcResponseError',
|
|
message: 'This is a custom error message from the server',
|
|
payload: {
|
|
encoding: 'application/json',
|
|
body: {
|
|
error: 'CustomError',
|
|
message: 'This is a custom error message from the server',
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it('turns 401 responses into XrpcAuthenticationError', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json(
|
|
{
|
|
error: 'Unauthorized',
|
|
message: 'Unauthorized access',
|
|
},
|
|
{
|
|
status: 401,
|
|
headers: {
|
|
'www-authenticate':
|
|
'Basic realm="example", charset="UTF-8", error="invalid_token", error_description="The access token is invalid", Bearer realm="oauth"',
|
|
},
|
|
},
|
|
)
|
|
})
|
|
|
|
const client = new Client({ fetchHandler })
|
|
|
|
const response = await client.xrpcSafe(app.bsky.actor.getPreferences)
|
|
|
|
assert(response instanceof XrpcAuthenticationError)
|
|
expect(response.error).toBe('Unauthorized')
|
|
expect(response.message).toBe('Unauthorized access')
|
|
expect(response.name).toBe('XrpcAuthenticationError')
|
|
expect(response.wwwAuthenticate).toEqual({
|
|
Basic: {
|
|
realm: 'example',
|
|
charset: 'UTF-8',
|
|
error: 'invalid_token',
|
|
error_description: 'The access token is invalid',
|
|
},
|
|
Bearer: {
|
|
realm: 'oauth',
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('records', () => {
|
|
it('allows creating records', async () => {
|
|
let currentTid = 0
|
|
// Only works 8 times
|
|
const nextTid = vi.fn(() => `2222222222${2 + currentTid++}22`)
|
|
|
|
const did = 'did:plc:alice'
|
|
const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {
|
|
expect(url).toBe('/xrpc/com.atproto.repo.createRecord')
|
|
expect(init?.method).toBe('POST')
|
|
expect(typeof init?.body).toBe('string')
|
|
const payload = com.atproto.repo.createRecord.main.input.schema.parse(
|
|
lexParse(init?.body as string),
|
|
)
|
|
|
|
expect(payload).toMatchObject({
|
|
repo: did,
|
|
collection: expect.any(String),
|
|
record: expect.any(Object),
|
|
})
|
|
|
|
const rkey = payload.rkey || nextTid()
|
|
const cid = await cidForLex(payload.record as LexValue)
|
|
|
|
const responseBody: com.atproto.repo.createRecord.$OutputBody = {
|
|
cid: cid.toString(),
|
|
uri: `at://${payload.repo}/${payload.collection}/${rkey}`,
|
|
}
|
|
|
|
return Response.json(responseBody)
|
|
})
|
|
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await expect(async () => {
|
|
return client.create(
|
|
app.bsky.feed.generator,
|
|
{
|
|
// @ts-expect-error invalid DID
|
|
did: 'not-a-did',
|
|
displayName: 'Alice Generator',
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
rkey: 'alice-generator',
|
|
validateRequest: true,
|
|
},
|
|
)
|
|
}).rejects.toThrow('Invalid DID (got "not-a-did") at $.did')
|
|
|
|
// validate performs schema validation before making the request
|
|
expect(fetchHandler).toHaveBeenCalledTimes(0)
|
|
|
|
const newGenerator = await client.create(
|
|
app.bsky.feed.generator,
|
|
{
|
|
did,
|
|
displayName: 'Alice Generator',
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
rkey: 'alice-generator',
|
|
validate: true,
|
|
},
|
|
)
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(1)
|
|
expect(newGenerator.cid).toBe(
|
|
'bafyreihx5eurnmsnj6ulfby3icl4ebh6pliwuqaze25z4ejitnt23b4vw4',
|
|
)
|
|
|
|
const aliceGenerator = await client.create(app.bsky.feed.generator, {
|
|
did,
|
|
displayName: 'Alice Generator',
|
|
createdAt: toDatetimeString(new Date()),
|
|
})
|
|
|
|
expect(nextTid).toHaveBeenCalledTimes(1)
|
|
expect(aliceGenerator.uri).toBe(
|
|
`at://${did}/app.bsky.feed.generator/${'2'.repeat(13)}`,
|
|
)
|
|
|
|
const newProfile = await client.create(app.bsky.actor.profile, {
|
|
displayName: 'Alice',
|
|
})
|
|
|
|
expect(nextTid).toHaveBeenCalledTimes(1)
|
|
expect(fetchHandler).toHaveBeenCalledTimes(3)
|
|
expect(newProfile.uri).toBe(
|
|
'at://did:plc:alice/app.bsky.actor.profile/self',
|
|
)
|
|
|
|
const newPost = await client.create(app.bsky.feed.post, {
|
|
text: 'Hello, world!',
|
|
createdAt: toDatetimeString(new Date()),
|
|
})
|
|
|
|
expect(nextTid).toHaveBeenCalledTimes(2)
|
|
expect(fetchHandler).toHaveBeenCalledTimes(4)
|
|
expect(newPost.uri).toBe(`at://${did}/app.bsky.feed.post/2222222222322`)
|
|
})
|
|
|
|
it('allows fetching records', async () => {
|
|
const did = 'did:plc:alice'
|
|
const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {
|
|
expect(init?.method).toBe('GET')
|
|
const urlObj = new URL(url, 'https://example.com')
|
|
expect(urlObj.pathname).toBe('/xrpc/com.atproto.repo.getRecord')
|
|
|
|
const repo = urlObj.searchParams.get('repo')
|
|
const collection = urlObj.searchParams.get('collection')
|
|
const rkey = urlObj.searchParams.get('rkey')
|
|
|
|
expect(repo).toBe(did)
|
|
expect(collection).toBe(app.bsky.feed.post.$type)
|
|
expect(rkey).toBe('2222222222222')
|
|
|
|
const record = app.bsky.feed.post.$build({
|
|
text: 'This is an old post',
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
})
|
|
|
|
const cid = await cidForLex(record)
|
|
|
|
const responseBody: com.atproto.repo.getRecord.$OutputBody = {
|
|
cid: cid.toString(),
|
|
uri: `at://${repo!}/${collection!}/${rkey!}` as any,
|
|
value: record,
|
|
}
|
|
|
|
return Response.json(responseBody)
|
|
})
|
|
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
const { value: post } = await client.get(app.bsky.feed.post, {
|
|
rkey: '2222222222222',
|
|
})
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(1)
|
|
expect(post).toMatchObject({
|
|
$type: 'app.bsky.feed.post',
|
|
text: 'This is an old post',
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
})
|
|
|
|
// @TODO: using getRecord method (to check we got the cid)
|
|
})
|
|
})
|
|
|
|
describe('blobs', () => {
|
|
const fetchHandler = vi.fn(
|
|
async (url: string, init?: RequestInit): Promise<Response> => {
|
|
expect(url).toBe('/xrpc/com.atproto.repo.uploadBlob')
|
|
expect(init?.method).toBe('POST')
|
|
const headers = new Headers(init?.headers)
|
|
const type = headers.get('content-type')!
|
|
expect(type).toBeDefined()
|
|
const blob =
|
|
init?.body instanceof Blob
|
|
? init.body
|
|
: ArrayBuffer.isView(init?.body) ||
|
|
init?.body instanceof ArrayBuffer
|
|
? new Blob([init.body], { type })
|
|
: (() => {
|
|
throw new Error('Invalid body type')
|
|
})()
|
|
|
|
const bytes = new Uint8Array(await blob.arrayBuffer())
|
|
|
|
const responseBody: com.atproto.repo.uploadBlob.$OutputBody = {
|
|
blob: {
|
|
$type: 'blob',
|
|
ref: await cidForRawBytes(bytes),
|
|
mimeType: blob.type,
|
|
size: blob.size,
|
|
},
|
|
}
|
|
|
|
return Response.json(lexToJson(responseBody))
|
|
},
|
|
)
|
|
|
|
it('allows uploading blobs', async () => {
|
|
const client = new Client({ fetchHandler })
|
|
const blob = new Blob(['hello world'], { type: 'text/plain' })
|
|
|
|
const { body } = await client.uploadBlob(blob)
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(1)
|
|
assert(isTypedBlobRef(body.blob))
|
|
expect(body.blob.$type).toBe('blob')
|
|
expect(body.blob.mimeType).toBe('text/plain')
|
|
expect(body.blob.size).toBe(11)
|
|
expect(body.blob.ref).toEqual(
|
|
await cidForRawBytes(new TextEncoder().encode('hello world')),
|
|
)
|
|
})
|
|
|
|
it('allows uploading blobs from Uint8Array', async () => {
|
|
const client = new Client({ fetchHandler })
|
|
const data = new TextEncoder().encode('hello world')
|
|
|
|
const { body } = await client.uploadBlob(data)
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(2)
|
|
assert(isTypedBlobRef(body.blob))
|
|
expect(body.blob.$type).toBe('blob')
|
|
expect(body.blob.mimeType).toBe('application/octet-stream')
|
|
expect(body.blob.size).toBe(11)
|
|
expect(body.blob.ref).toEqual(
|
|
await cidForRawBytes(new TextEncoder().encode('hello world')),
|
|
)
|
|
})
|
|
|
|
it('allows uploading blobs from ArrayBuffer', async () => {
|
|
const client = new Client({ fetchHandler })
|
|
const data = new TextEncoder().encode('hello world').buffer
|
|
|
|
const { body } = await client.uploadBlob(data)
|
|
|
|
expect(fetchHandler).toHaveBeenCalledTimes(3)
|
|
assert(isTypedBlobRef(body.blob))
|
|
expect(body.blob.$type).toBe('blob')
|
|
expect(body.blob.mimeType).toBe('application/octet-stream')
|
|
expect(body.blob.size).toBe(11)
|
|
expect(body.blob.ref).toEqual(
|
|
await cidForRawBytes(new TextEncoder().encode('hello world')),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('validateRequest option', () => {
|
|
const did = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz' as const
|
|
|
|
describe('create()', () => {
|
|
it('validates locally when validateRequest: true', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>()
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await expect(
|
|
client.create(
|
|
app.bsky.feed.generator,
|
|
{
|
|
// @ts-expect-error invalid DID
|
|
did: 'not-a-did',
|
|
displayName: 'Test',
|
|
createdAt: toDatetimeString(new Date()),
|
|
},
|
|
{ rkey: 'test', validateRequest: true },
|
|
),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof LexValidationError)
|
|
expect(err.message).toMatch('Invalid DID')
|
|
return true
|
|
})
|
|
|
|
expect(fetchHandler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips local validation when validateRequest: false', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json({
|
|
uri: `at://${did}/app.bsky.feed.generator/test`,
|
|
cid: cborCid.toString(),
|
|
})
|
|
})
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await client.create(
|
|
app.bsky.feed.generator,
|
|
{
|
|
// @ts-expect-error invalid DID
|
|
did: 'not-a-did',
|
|
displayName: 'Test',
|
|
createdAt: toDatetimeString(new Date()),
|
|
},
|
|
{ rkey: 'test', validateRequest: false },
|
|
)
|
|
|
|
expect(fetchHandler).toHaveBeenCalled()
|
|
})
|
|
|
|
it('defaults to not validating', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json({
|
|
uri: `at://${did}/app.bsky.feed.generator/test`,
|
|
cid: cborCid.toString(),
|
|
})
|
|
})
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await client.create(
|
|
app.bsky.feed.generator,
|
|
{
|
|
// @ts-expect-error invalid DID
|
|
did: 'not-a-did',
|
|
displayName: 'Test',
|
|
createdAt: toDatetimeString(new Date()),
|
|
},
|
|
{ rkey: 'test' },
|
|
)
|
|
|
|
expect(fetchHandler).toHaveBeenCalled()
|
|
})
|
|
|
|
it('validates required fields when validateRequest: true', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>()
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await expect(
|
|
client.create(
|
|
app.bsky.feed.generator,
|
|
// @ts-expect-error
|
|
{
|
|
displayName: 'Test',
|
|
},
|
|
{ rkey: 'test', validateRequest: true },
|
|
),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof LexValidationError)
|
|
expect(err.message).toMatch('Missing required key "did"')
|
|
return true
|
|
})
|
|
|
|
expect(fetchHandler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('validates types when validateRequest: true', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>()
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await expect(
|
|
client.create(
|
|
app.bsky.feed.generator,
|
|
{
|
|
did,
|
|
// @ts-expect-error wrong type
|
|
displayName: 123,
|
|
createdAt: toDatetimeString(new Date()),
|
|
},
|
|
{ rkey: 'test', validateRequest: true },
|
|
),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof LexValidationError)
|
|
expect(err.message).toMatch('Expected string value type (got 123)')
|
|
return true
|
|
})
|
|
|
|
expect(fetchHandler).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('put()', () => {
|
|
it('validates locally when validateRequest: true', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>()
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await expect(
|
|
client.put(
|
|
app.bsky.actor.profile,
|
|
{
|
|
// @ts-expect-error invalid data
|
|
displayName: 123,
|
|
},
|
|
{ validateRequest: true },
|
|
),
|
|
).rejects.toSatisfy((err) => {
|
|
assert(err instanceof LexValidationError)
|
|
expect(err.message).toMatch('Expected string value type (got 123)')
|
|
return true
|
|
})
|
|
|
|
expect(fetchHandler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips local validation when validateRequest: false', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json({
|
|
uri: `at://${did}/app.bsky.actor.profile/self`,
|
|
cid: cborCid.toString(),
|
|
})
|
|
})
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await client.put(
|
|
app.bsky.actor.profile,
|
|
{
|
|
// @ts-expect-error invalid data
|
|
displayName: 123,
|
|
},
|
|
{ validateRequest: false },
|
|
)
|
|
|
|
expect(fetchHandler).toHaveBeenCalled()
|
|
})
|
|
|
|
it('defaults to not validating', async () => {
|
|
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
return Response.json({
|
|
uri: `at://${did}/app.bsky.actor.profile/self`,
|
|
cid: cborCid.toString(),
|
|
})
|
|
})
|
|
const client = new Client({ fetchHandler, did })
|
|
|
|
await client.put(app.bsky.actor.profile, {
|
|
// @ts-expect-error invalid data
|
|
displayName: 123,
|
|
})
|
|
|
|
expect(fetchHandler).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
})
|