atproto/packages/ozone/tests/moderation-statuses.test.ts
Foysal Ahamed 19e36afb2c
Add collection and subjectType filters to queryEvents and queryStatuses (#2914)
*  Add collection and subjectType filters to queryEvents and queryStatuses

* 📝 Add changeset

* ♻️ Refactor or query builder
2024-10-30 16:20:38 +01:00

400 lines
12 KiB
TypeScript

import assert from 'node:assert'
import {
TestNetwork,
SeedClient,
basicSeed,
ModeratorClient,
} from '@atproto/dev-env'
import {
ToolsOzoneModerationDefs,
ToolsOzoneModerationQueryStatuses,
} from '@atproto/api'
import { forSnapshot } from './_util'
import {
REASONMISLEADING,
REASONSPAM,
} from '../src/lexicon/types/com/atproto/moderation/defs'
import {
REVIEWOPEN,
REVIEWNONE,
} from '../src/lexicon/types/tools/ozone/moderation/defs'
describe('moderation-statuses', () => {
let network: TestNetwork
let sc: SeedClient
let modClient: ModeratorClient
const seedEvents = async () => {
const bobsAccount = {
$type: 'com.atproto.admin.defs#repoRef',
did: sc.dids.bob,
}
const carlasAccount = {
$type: 'com.atproto.admin.defs#repoRef',
did: sc.dids.alice,
}
const bobsPost = {
$type: 'com.atproto.repo.strongRef',
uri: sc.posts[sc.dids.bob][0].ref.uriStr,
cid: sc.posts[sc.dids.bob][0].ref.cidStr,
}
const alicesPost = {
$type: 'com.atproto.repo.strongRef',
uri: sc.posts[sc.dids.alice][1].ref.uriStr,
cid: sc.posts[sc.dids.alice][1].ref.cidStr,
}
for (let i = 0; i < 4; i++) {
await sc.createReport({
reasonType: i % 2 ? REASONSPAM : REASONMISLEADING,
reason: 'X',
// Report bob's account by alice and vice versa
subject: i % 2 ? bobsAccount : carlasAccount,
reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob,
})
await sc.createReport({
reasonType: REASONSPAM,
reason: 'X',
// Report bob's post by alice and vice versa
subject: i % 2 ? bobsPost : alicesPost,
reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob,
})
}
}
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'ozone_moderation_statuses',
})
sc = network.getSeedClient()
modClient = network.ozone.getModClient()
await basicSeed(sc)
await network.processAll()
await seedEvents()
})
afterAll(async () => {
await network.close()
})
describe('query statuses', () => {
it('returns statuses for subjects that received moderation events', async () => {
const response = await modClient.queryStatuses({})
expect(forSnapshot(response.subjectStatuses)).toMatchSnapshot()
})
it('returns statuses filtered by subject language', async () => {
const klingonQueue = await modClient.queryStatuses({
tags: ['lang:i'],
})
expect(forSnapshot(klingonQueue.subjectStatuses)).toMatchSnapshot()
const nonKlingonQueue = await modClient.queryStatuses({
excludeTags: ['lang:i'],
})
// Verify that the klingon tagged subject is not returned when excluding klingon
expect(nonKlingonQueue.subjectStatuses.map((s) => s.id)).not.toContain(
klingonQueue.subjectStatuses[0].id,
)
// Verify multi lang tag exclusion
Promise.all(
nonKlingonQueue.subjectStatuses.map((s, i) => {
return modClient.emitEvent({
subject: s.subject,
event: {
$type: 'tools.ozone.moderation.defs#modEventTag',
add: [i % 2 ? 'lang:jp' : 'lang:it'],
remove: [],
comment: 'Adding custom lang tag',
},
createdBy: sc.dids.alice,
})
}),
)
const queueWithoutKlingonAndItalian = await modClient.queryStatuses({
excludeTags: ['lang:i', 'lang:it'],
})
queueWithoutKlingonAndItalian.subjectStatuses
.map((s) => s.tags)
.flat()
.forEach((tag) => {
expect(['lang:it', 'lang:i']).not.toContain(tag)
})
})
it('returns paginated statuses', async () => {
// We know there will be exactly 4 statuses in db
const getPaginatedStatuses = async (
params: ToolsOzoneModerationQueryStatuses.QueryParams,
) => {
let cursor: string | undefined = ''
const statuses: ToolsOzoneModerationDefs.SubjectStatusView[] = []
let count = 0
do {
const results = await modClient.queryStatuses({
limit: 1,
cursor,
...params,
})
cursor = results.cursor
statuses.push(...results.subjectStatuses)
count++
// The count is just a brake-check to prevent infinite loop
} while (cursor && count < 10)
return statuses
}
const list = await getPaginatedStatuses({})
expect(list[0].id).toEqual(7)
expect(list[list.length - 1].id).toEqual(1)
await modClient.emitEvent({
subject: list[1].subject,
event: {
$type: 'tools.ozone.moderation.defs#modEventAcknowledge',
comment: 'X',
},
})
const listReviewedFirst = await getPaginatedStatuses({
sortDirection: 'desc',
sortField: 'lastReviewedAt',
})
// Verify that the item that was recently reviewed comes up first when sorted descendingly
// while the result set always contains same number of items regardless of sorting
expect(listReviewedFirst[0].id).toEqual(list[1].id)
expect(listReviewedFirst.length).toEqual(list.length)
})
it('returns statuses for specified collections', async () => {
const sp = await sc.createStarterPack(
sc.dids.alice,
"alice's about to get blocked starter pack",
[sc.dids.bob, sc.dids.carol],
[],
)
await sc.createReport({
reasonType: REASONSPAM,
reason: 'X',
subject: {
$type: 'com.atproto.repo.strongRef',
...sp.raw,
},
reportedBy: sc.dids.bob,
})
const [
onlyStarterPackStatuses,
onlyAlicesStarterPackStatuses,
onlyBobsStarterPackStatuses,
onlyPostStatuses,
] = await Promise.all([
modClient.queryStatuses({
collections: ['app.bsky.graph.starterpack'],
}),
modClient.queryStatuses({
subject: sc.dids.alice,
includeAllUserRecords: true,
collections: ['app.bsky.graph.starterpack'],
}),
modClient.queryStatuses({
subject: sc.dids.bob,
includeAllUserRecords: true,
collections: ['app.bsky.graph.starterpack'],
}),
modClient.queryStatuses({
collections: ['app.bsky.feed.post'],
}),
])
expect(onlyStarterPackStatuses.subjectStatuses.length).toEqual(1)
expect(onlyStarterPackStatuses.subjectStatuses[0].subject.uri).toContain(
'app.bsky.graph.starterpack',
)
expect(onlyAlicesStarterPackStatuses.subjectStatuses.length).toEqual(1)
expect(
onlyAlicesStarterPackStatuses.subjectStatuses[0].subject.uri,
).toEqual(sp.uriStr)
expect(onlyBobsStarterPackStatuses.subjectStatuses.length).toEqual(0)
expect(onlyPostStatuses.subjectStatuses.length).toEqual(2)
})
it('returns statuses for account or records', async () => {
const [
onlyAccountStatuses,
onlyRecordStatuses,
onlyStatusesOnBobsAccount,
] = await Promise.all([
modClient.queryStatuses({
subjectType: 'account',
}),
modClient.queryStatuses({
subjectType: 'record',
}),
modClient.queryStatuses({
subject: sc.dids.bob,
subjectType: 'record',
}),
])
// only account statuses are returned, no event has a uri
expect(
onlyAccountStatuses.subjectStatuses.every((e) => !e.subject.uri),
).toBeTruthy()
// only record statuses are returned, all events have a uri
expect(
onlyRecordStatuses.subjectStatuses.every((e) => e.subject.uri),
).toBeTruthy()
// only bob's account statuses are returned, no events have a URI even though the subjectType is record
expect(
onlyStatusesOnBobsAccount.subjectStatuses.every(
(e) => !e.subject.uri && e.subject.did === sc.dids.bob,
),
).toBeTruthy()
})
})
describe('reviewState changes', () => {
it('only sets state to #reviewNone on first non-impactful event', async () => {
const bobsAccount = {
$type: 'com.atproto.admin.defs#repoRef',
did: sc.dids.bob,
}
const alicesPost = {
$type: 'com.atproto.repo.strongRef',
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
cid: sc.posts[sc.dids.alice][0].ref.cidStr,
}
const getBobsAccountStatus = async () => {
const data = await modClient.queryStatuses({
subject: bobsAccount.did,
})
return data.subjectStatuses[0]
}
// Since bob's account already had a reviewState, it won't be changed by non-impactful events
const bobsAccountStatusBeforeTag = await getBobsAccountStatus()
await Promise.all([
modClient.emitEvent({
subject: bobsAccount,
event: {
$type: 'tools.ozone.moderation.defs#modEventTag',
add: ['newTag'],
remove: [],
comment: 'X',
},
createdBy: sc.dids.alice,
}),
modClient.emitEvent({
subject: bobsAccount,
event: {
$type: 'tools.ozone.moderation.defs#modEventComment',
comment: 'X',
},
createdBy: sc.dids.alice,
}),
])
const bobsAccountStatusAfterTag = await getBobsAccountStatus()
expect(bobsAccountStatusBeforeTag.reviewState).toEqual(
bobsAccountStatusAfterTag.reviewState,
)
// Since alice's post didn't have a reviewState it is set to reviewNone on first non-impactful event
const getAlicesPostStatus = async () => {
const data = await modClient.queryStatuses({
subject: alicesPost.uri,
})
return data.subjectStatuses[0]
}
const alicesPostStatusBeforeTag = await getAlicesPostStatus()
expect(alicesPostStatusBeforeTag).toBeUndefined()
await modClient.emitEvent({
subject: alicesPost,
event: {
$type: 'tools.ozone.moderation.defs#modEventComment',
comment: 'X',
},
createdBy: sc.dids.alice,
})
const alicesPostStatusAfterTag = await getAlicesPostStatus()
expect(alicesPostStatusAfterTag.reviewState).toEqual(REVIEWNONE)
await modClient.emitEvent({
subject: alicesPost,
event: {
$type: 'tools.ozone.moderation.defs#modEventReport',
reportType: REASONMISLEADING,
comment: 'X',
},
createdBy: sc.dids.alice,
})
const alicesPostStatusAfterReport = await getAlicesPostStatus()
expect(alicesPostStatusAfterReport.reviewState).toEqual(REVIEWOPEN)
})
})
describe('blobs', () => {
it('are tracked on takendown subject', async () => {
const post = sc.posts[sc.dids.carol][0]
assert(post.images.length > 1)
await modClient.emitEvent({
event: {
$type: 'tools.ozone.moderation.defs#modEventTakedown',
},
subject: {
$type: 'com.atproto.repo.strongRef',
uri: post.ref.uriStr,
cid: post.ref.cidStr,
},
subjectBlobCids: [post.images[0].image.ref.toString()],
createdBy: sc.dids.alice,
})
const result = await modClient.queryStatuses({
subject: post.ref.uriStr,
})
expect(result.subjectStatuses.length).toBe(1)
expect(result.subjectStatuses[0]).toMatchObject({
takendown: true,
subjectBlobCids: [post.images[0].image.ref.toString()],
})
})
it('are tracked on reverse-takendown subject based on previous status', async () => {
const post = sc.posts[sc.dids.carol][0]
await modClient.emitEvent({
event: {
$type: 'tools.ozone.moderation.defs#modEventReverseTakedown',
},
subject: {
$type: 'com.atproto.repo.strongRef',
uri: post.ref.uriStr,
cid: post.ref.cidStr,
},
})
const result = await modClient.queryStatuses({
subject: post.ref.uriStr,
})
expect(result.subjectStatuses.length).toBe(1)
expect(result.subjectStatuses[0]).toMatchObject({
takendown: false,
subjectBlobCids: [post.images[0].image.ref.toString()],
})
})
})
})