atproto/packages/ozone/tests/query-labels.test.ts
Matthieu Sieben b934b396b1
Client SDK rework (#2483)
* feat(api): support creation of oauth based AtpAgents

* oauth: misc fixes for confidential clients

* fix(xprc): remove ReadableStream.from polyfill

* OAuth docs tweaks (#2679)

* OAuth: clarification about client_name being shown

* OAuth: re-write handle resolution privacy concern

* avoid relying on ReadableStream.from in xrpc-server tests

* feat(oauth-types): expose "ALLOW_UNSECURE_ORIGINS" constant

* feat(handle-resolver): expose "AtprotoIdentityDidMethods" type

* fix(oauth-client): ensure that the oauth metadata document contains client_id_metadata_document_supported

* fix(oauth-types): prevent unknown query string in loopback client id

* fix(identity-resolver): check that handle is in did doc's "alsoKnownAs"

* feat(oauth-client:oauth-resolver): allow logging in using either the PDS URL or Entryway URL

* fix(oauth-client): return better error in case of invalid "oauth-protected-resource" status code

* refactor(did): group atproto specific checks in own

* feat(api): relax typing of "appLabelers" and "labelers" AtpClient properties

* allow any did as labeller (for tests mainly)

* fix(api): allow to override "atproto-proxy" on a per-request basis

* remove release candidate versions from changelog

* update changeset for api and xrpc packages

* Add missing changeset

* revert RC versions

* Proper wording in OAUTH.md api example

* remove "pre" changeset file

* xrpc: restore original behavior of setHEader and unsetHeader

* docs: add comment for XrpcClient 's constructor arg

* feat(api): expose "schemas" publicly

* feat(api): allow customizing the whatwg fetch function of the AtpAgent

* docs(api): improve migration docs

* docs: change reference to BskyAgent to AtpAgent

* docs: mention the breaking change regarding setSessionPersistHandler

* fix(api): better split AtpClient concerns

* fix(xrpc): remove unused import

* refactor(api): simplify class hierarchu by removeing AtpClient

* fix(api): mock proper method for facets detection

* restore ability to restore session asynchronously

* feat(api): allow instantiating Agent with same argument as super class

* docs(api): properly extend Agent class

* style(xrpc): var name

* docs(api): remove "async" to header getter

---------

Co-authored-by: Devin Ivy <devinivy@gmail.com>
Co-authored-by: bnewbold <bnewbold@robocracy.org>
Co-authored-by: Hailey <me@haileyok.com>
2024-08-12 19:57:21 +02:00

236 lines
7.0 KiB
TypeScript

import { AtpAgent } from '@atproto/api'
import { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env'
import { DisconnectError, Subscription } from '@atproto/xrpc-server'
import { ids, lexicons } from '../src/lexicon/lexicons'
import { Label } from '../src/lexicon/types/com/atproto/label/defs'
import { Secp256k1Keypair, verifySignature } from '@atproto/crypto'
import { cborEncode } from '@atproto/common'
import { ModerationService } from '../src/mod-service'
import {
OutputSchema as LabelMessage,
isLabels,
} from '../src/lexicon/types/com/atproto/label/subscribeLabels'
import { getSigningKeyId } from '../src/util'
describe('ozone query labels', () => {
let network: TestNetwork
let agent: AtpAgent
let labels: Label[]
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'ozone_query_labels',
})
agent = network.ozone.getClient()
const toCreate = [
{
src: EXAMPLE_LABELER,
uri: 'did:example:blah',
val: 'spam',
cts: new Date().toISOString(),
},
{
src: EXAMPLE_LABELER,
uri: 'did:example:blah',
val: 'impersonation',
cts: new Date().toISOString(),
},
{
src: EXAMPLE_LABELER,
uri: 'at://did:example:blah/app.bsky.feed.post/1234abcde',
val: 'spam',
cts: new Date().toISOString(),
},
{
src: EXAMPLE_LABELER,
uri: 'at://did:example:blah/app.bsky.feed.post/1234abcfg',
val: 'spam',
cts: new Date().toISOString(),
},
{
src: EXAMPLE_LABELER,
uri: 'at://did:example:blah/app.bsky.actor.profile/self',
val: 'spam',
cts: new Date().toISOString(),
},
{
src: EXAMPLE_LABELER,
uri: 'did:example:thing',
val: 'spam',
cts: new Date().toISOString(),
},
]
const modService = network.ozone.ctx.modService(network.ozone.ctx.db)
labels = await modService.createLabels(toCreate)
})
afterAll(async () => {
await network.close()
})
it('returns all labels', async () => {
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['*'],
})
expect(res.data.labels).toEqual(labels)
})
it('returns all labels even when an additional pattern is supplied', async () => {
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['*', 'did:example:blah'],
})
expect(res.data.labels).toEqual(labels)
})
it('returns all labels that match an exact uri pattern', async () => {
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['did:example:blah'],
})
expect(res.data.labels).toEqual(labels.slice(0, 2))
})
it('returns all labels that match one of multiple exact uris', async () => {
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: [
'at://did:example:blah/app.bsky.feed.post/1234abcfg',
'at://did:example:blah/app.bsky.actor.profile/self',
],
})
expect(res.data.labels).toEqual(labels.slice(3, 5))
})
it('returns all labels that match one of multiple uris, exact & glob', async () => {
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],
})
expect(res.data.labels).toEqual(labels.slice(0, 5))
})
it('paginates', async () => {
const res1 = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],
limit: 3,
})
const res2 = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],
limit: 3,
cursor: res1.data.cursor,
})
expect([...res1.data.labels, ...res2.data.labels]).toEqual(
labels.slice(0, 5),
)
})
it('returns validly signed labels', async () => {
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['*'],
})
const signingKey = network.ozone.ctx.signingKey.did()
for (const label of res.data.labels) {
const { sig, ...rest } = label
if (!sig) {
throw new Error('Missing signature')
}
const encodedLabel = cborEncode(rest)
const isValid = await verifySignature(signingKey, encodedLabel, sig)
expect(isValid).toBe(true)
}
})
it('resigns labels if the signingKey changes', async () => {
// mock changing the signing key for the service
const ctx = network.ozone.ctx
const origModServiceFn = ctx.modService
const modSrvc = ctx.modService(ctx.db)
const newSigningKey = await Secp256k1Keypair.create()
const newSigningKeyId = await getSigningKeyId(ctx.db, newSigningKey.did())
ctx.devOverride({
// @ts-ignore
modService: ModerationService.creator(
newSigningKey,
newSigningKeyId,
ctx.cfg,
modSrvc.backgroundQueue,
ctx.idResolver,
// @ts-ignore
modSrvc.eventPusher,
modSrvc.appviewAgent,
ctx.serviceAuthHeaders,
),
})
const res = await agent.api.com.atproto.label.queryLabels({
uriPatterns: ['*'],
})
for (const label of res.data.labels) {
const { sig, ...rest } = label
if (!sig) {
throw new Error('Missing signature')
}
const encodedLabel = cborEncode(rest)
const isValid = await verifySignature(
newSigningKey.did(),
encodedLabel,
sig,
)
expect(isValid).toBe(true)
}
await network.ozone.processAll()
const fromDb = await ctx.db.db.selectFrom('label').selectAll().execute()
expect(fromDb.every((row) => row.signingKeyId === newSigningKeyId)).toBe(
true,
)
ctx.devOverride({
modService: origModServiceFn,
})
})
describe('subscribeLabels', () => {
it('streams all labels from initial cursor.', async () => {
const ac = new AbortController()
let doneTimer: NodeJS.Timeout
const resetDoneTimer = () => {
clearTimeout(doneTimer)
doneTimer = setTimeout(() => ac.abort(new DisconnectError()), 100)
}
const sub = new Subscription({
signal: ac.signal,
service: agent.service.origin.replace('http://', 'ws://'),
method: ids.ComAtprotoLabelSubscribeLabels,
getParams() {
return { cursor: 0 }
},
validate(obj) {
return lexicons.assertValidXrpcMessage<LabelMessage>(
ids.ComAtprotoLabelSubscribeLabels,
obj,
)
},
})
const streamedLabels: Label[] = []
for await (const message of sub) {
resetDoneTimer()
if (isLabels(message)) {
for (const label of message.labels) {
// sigs are currently parsed as a Buffer which is a Uint8Array under the hood, but fails our equality test so we cast to Uint8Array
streamedLabels.push({
...label,
sig: label.sig ? new Uint8Array(label.sig) : undefined,
})
}
}
}
expect(streamedLabels).toEqual(labels)
})
})
})