atproto/packages/api/tests/util/moderation-behavior.ts
Matthieu Sieben f689bd51a2
Build system rework (#2169)
* refactor(crypto): remove circular dependency

* refactor(crypto): expose compress/decompress as part of the DidKeyPlugin interface

* fix(crypto): remove import from private file

* refactor: isolate tsconfig

* fix: remove unused bench file

* chore(repo): remove unused deps

* fix(ozone): properly list dependencies

* fix(services): do lint js files

* fix(services/pds): remove unused deps

* chore(pds): remove bench

* chore(dev-env): remove unused deps

* chore(api): remove bench

* remove unused babel.config.js files

* fix: remove .ts extension from import

* fix(pds): remove imports of src files

* fix(tsconfig): properly list all projects

* fix(dev-env): remove imports of src files

* fix(bsky): remove direct import to crypto src

* fix(api): remove imports to api internals

* chore(build): prevent bundling of built output

* chore(dev): add "dev" script to build in watch mode

* chore(deps): move ts-node dependency where it is actually used

* fix(deps): add dev-env as project dependency

* fix(xrpc-server): properly type kexicon

* fix(bsky): improve typings

* fix(pds): fully type formatRecordEmbedInternal return value

* fix(repo): remove imports from @ipld/car/api

* feat(dev-env): re-export BskyIngester

* fix: properly lint & type jest config & test files

* fix(ci): test after build

* fix(types): use NodeJS.Timeout instead of NodeJS.Timer

* fix(bsky): make types exportable

* fix(ozone): make types exportable

* fix(xrpc-server): make types exportable

* fix(xprc-server): make code compliant with "node" types

* fix(xrpc-server): avoid accessing properties of unknown

* chore(deps): update @types/node

* feat(tsconfig): narrow down available types depending on the package's target environment

* fix(pds): remove unused prop

* fix(bsync): Database's migrator not always initialized

* fix(dev-env): remove unreachable code

* fix(xrpc-server): remove unused import

* fix(xrpc-server): mark header property as abstract

* fix(pds): initialize LeakyTxPlugin's txOver property

* fix(bsky): initialize LeakyTxPlugin's txOver property

* fix(bsky): remove unused migrator from DatabaseCoordinator

* fix(bsky): Properly initialize LabelService's cache property

* fix(ozone): Database's migrator not initialized

* fix(ozone): initialize LeakyTxPlugin's txOver property

* fix(crypto): ignore unused variable error

* feat(tsconfig): use stricter rules

* feat(tsconfig): enable useDefineForClassFields

* feat(xrpc-server): add support for brotli incoming payload

* fix(xrpc-server): properly parse & process content-encoding

* fix(common:stream): always call cb in _transform

* tidy/fix tests and service entrypoints

* Revert "fix(xrpc-server): properly parse & process content-encoding"

This reverts commit 2b1c66e153820d3e128fc839fcc1834d52a66686.

* Revert "feat(xrpc-server): add support for brotli incoming payload"

This reverts commit e710c21e6118214ddf215b0515e68cb87299a952.

* remove special node env for tests (defaults to jest val of "test")

* kill mute sync handler on disconnect

* work around connect-es bug w/ request aborts

* style(crypto): rename imports from uint8arrays

* fix update package-lock

* fix lint

* force hbs files to be bundled as cjs

* fix: use concurrently instead of npm-run-all

npm-run-all seems not to be maintained anymore. Additionally, concurrently better forwards signals to child processes.

* remove concurrently alltogether

* ignore sqlite files in services/pds

* fix verify

* fix verify

* tidy, fix verify

* fix blob diversion test

* build rework changeset

---------

Co-authored-by: Devin Ivy <devinivy@gmail.com>
2024-03-18 17:10:58 -04:00

269 lines
7.6 KiB
TypeScript

import {
ModerationUI,
ModerationOpts,
ComAtprotoLabelDefs,
LabelPreference,
} from '../../src'
import { mock as m } from '../../src/mocker'
export type ModerationTestSuiteResultFlag =
| 'filter'
| 'blur'
| 'alert'
| 'inform'
| 'noOverride'
export interface ModerationTestSuiteScenario {
cfg: string
subject: 'post' | 'profile' | 'userlist' | 'feedgen'
author: string
quoteAuthor?: string
labels: {
post?: string[]
profile?: string[]
account?: string[]
quotedPost?: string[]
quotedAccount?: string[]
}
behaviors: {
profileList?: ModerationTestSuiteResultFlag[]
profileView?: ModerationTestSuiteResultFlag[]
avatar?: ModerationTestSuiteResultFlag[]
banner?: ModerationTestSuiteResultFlag[]
displayName?: ModerationTestSuiteResultFlag[]
contentList?: ModerationTestSuiteResultFlag[]
contentView?: ModerationTestSuiteResultFlag[]
contentMedia?: ModerationTestSuiteResultFlag[]
}
}
export type SuiteUsers = Record<
string,
{
blocking: boolean
blockingByList: boolean
blockedBy: boolean
muted: boolean
mutedByList: boolean
}
>
export type SuiteConfigurations = Record<
string,
{
authed?: boolean
adultContentEnabled?: boolean
settings?: Record<string, LabelPreference>
}
>
export type SuiteScenarios = Record<string, ModerationTestSuiteScenario>
expect.extend({
toBeModerationResult(
actual: ModerationUI,
expected: ModerationTestSuiteResultFlag[] | undefined,
context: string = '',
stringifiedResult: string | undefined = undefined,
_ignoreCause = false,
) {
const fail = (msg: string) => ({
pass: false,
message: () =>
`${msg}.${
stringifiedResult ? ` Full result: ${stringifiedResult}` : ''
}`,
})
// let cause = actual.causes?.type as string
// if (actual.cause?.type === 'label') {
// cause = `label:${actual.cause.labelDef.id}`
// } else if (actual.cause?.type === 'muted') {
// if (actual.cause.source.type === 'list') {
// cause = 'muted-by-list'
// }
// } else if (actual.cause?.type === 'blocking') {
// if (actual.cause.source.type === 'list') {
// cause = 'blocking-by-list'
// }
// }
if (!expected) {
// if (!ignoreCause && actual.cause) {
// return fail(`${context} expected to be a no-op, got ${cause}`)
// }
if (actual.inform) {
return fail(`${context} expected to be a no-op, got inform=true`)
}
if (actual.alert) {
return fail(`${context} expected to be a no-op, got alert=true`)
}
if (actual.blur) {
return fail(`${context} expected to be a no-op, got blur=true`)
}
if (actual.filter) {
return fail(`${context} expected to be a no-op, got filter=true`)
}
if (actual.noOverride) {
return fail(`${context} expected to be a no-op, got noOverride=true`)
}
} else {
// if (!ignoreCause && cause !== expected.cause) {
// return fail(`${context} expected to be ${expected.cause}, got ${cause}`)
// }
const expectedInform = expected.includes('inform')
if (!!actual.inform !== expectedInform) {
return fail(
`${context} expected to be inform=${expectedInform}, got ${
actual.inform || false
}`,
)
}
const expectedAlert = expected.includes('alert')
if (!!actual.alert !== expectedAlert) {
return fail(
`${context} expected to be alert=${expectedAlert}, got ${
actual.alert || false
}`,
)
}
const expectedBlur = expected.includes('blur')
if (!!actual.blur !== expectedBlur) {
return fail(
`${context} expected to be blur=${expectedBlur}, got ${
actual.blur || false
}`,
)
}
const expectedFilter = expected.includes('filter')
if (!!actual.filter !== expectedFilter) {
return fail(
`${context} expected to be filter=${expectedFilter}, got ${
actual.filter || false
}`,
)
}
const expectedNoOverride = expected.includes('noOverride')
if (!!actual.noOverride !== expectedNoOverride) {
return fail(
`${context} expected to be noOverride=${expectedNoOverride}, got ${
actual.noOverride || false
}`,
)
}
}
return { pass: true, message: () => '' }
},
})
export class ModerationBehaviorSuiteRunner {
constructor(
public users: SuiteUsers,
public configurations: SuiteConfigurations,
public scenarios: SuiteScenarios,
) {}
postScenario(scenario: ModerationTestSuiteScenario) {
if (scenario.subject !== 'post') {
throw new Error('Scenario subject must be "post"')
}
const author = this.profileViewBasic(scenario.author, scenario.labels)
return m.postView({
record: m.post({
text: 'Post text',
}),
author,
labels: (scenario.labels.post || []).map((val) =>
m.label({ val, uri: `at://${author.did}/app.bsky.feed.post/fake` }),
),
embed: scenario.quoteAuthor
? m.embedRecordView({
record: m.post({
text: 'Quoted post text',
}),
labels: (scenario.labels.quotedPost || []).map((val) =>
m.label({
val,
uri: `at://${author.did}/app.bsky.feed.post/fake`,
}),
),
author: this.profileViewBasic(scenario.quoteAuthor, {
account: scenario.labels.quotedAccount,
}),
})
: undefined,
})
}
profileScenario(scenario: ModerationTestSuiteScenario) {
if (scenario.subject !== 'profile') {
throw new Error('Scenario subject must be "profile"')
}
return this.profileViewBasic(scenario.author, scenario.labels)
}
profileViewBasic(
name: string,
scenarioLabels: ModerationTestSuiteScenario['labels'],
) {
const def = this.users[name]
const labels: ComAtprotoLabelDefs.Label[] = []
if (scenarioLabels.account) {
for (const l of scenarioLabels.account) {
labels.push(m.label({ val: l, uri: `did:web:${name}` }))
}
}
if (scenarioLabels.profile) {
for (const l of scenarioLabels.profile) {
labels.push(
m.label({
val: l,
uri: `at://did:web:${name}/app.bsky.actor.profile/self`,
}),
)
}
}
return m.profileViewBasic({
handle: `${name}.test`,
labels,
viewer: m.actorViewerState({
muted: def.muted || def.mutedByList,
mutedByList: def.mutedByList
? m.listViewBasic({ name: 'Fake List' })
: undefined,
blockedBy: def.blockedBy,
blocking:
def.blocking || def.blockingByList
? 'at://did:web:self.test/app.bsky.graph.block/fake'
: undefined,
blockingByList: def.blockingByList
? m.listViewBasic({ name: 'Fake List' })
: undefined,
}),
})
}
moderationOpts(scenario: ModerationTestSuiteScenario): ModerationOpts {
return {
userDid:
this.configurations[scenario.cfg].authed === false
? ''
: 'did:web:self.test',
prefs: {
adultContentEnabled: Boolean(
this.configurations[scenario.cfg]?.adultContentEnabled,
),
labels: this.configurations[scenario.cfg].settings || {},
labelers: [
{
did: 'did:plc:fake-labeler',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
}
}
}