mirror of
https://github.com/whekin/household-bot.git
synced 2026-04-01 01:44:02 +00:00
feat(bot): add anonymous feedback flow
This commit is contained in:
217
packages/application/src/anonymous-feedback-service.test.ts
Normal file
217
packages/application/src/anonymous-feedback-service.test.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import type {
|
||||
AnonymousFeedbackMemberRecord,
|
||||
AnonymousFeedbackRepository,
|
||||
AnonymousFeedbackSubmissionRecord
|
||||
} from '@household/ports'
|
||||
|
||||
import { createAnonymousFeedbackService } from './anonymous-feedback-service'
|
||||
|
||||
class AnonymousFeedbackRepositoryStub implements AnonymousFeedbackRepository {
|
||||
member: AnonymousFeedbackMemberRecord | null = {
|
||||
id: 'member-1',
|
||||
telegramUserId: '123',
|
||||
displayName: 'Stan'
|
||||
}
|
||||
|
||||
acceptedCountSince = 0
|
||||
lastAcceptedAt: Date | null = null
|
||||
duplicate = false
|
||||
created: Array<{
|
||||
rawText: string
|
||||
sanitizedText: string | null
|
||||
moderationStatus: string
|
||||
moderationReason: string | null
|
||||
}> = []
|
||||
posted: Array<{ submissionId: string; postedThreadId: string; postedMessageId: string }> = []
|
||||
failed: Array<{ submissionId: string; failureReason: string }> = []
|
||||
|
||||
async getMemberByTelegramUserId() {
|
||||
return this.member
|
||||
}
|
||||
|
||||
async getRateLimitSnapshot() {
|
||||
return {
|
||||
acceptedCountSince: this.acceptedCountSince,
|
||||
lastAcceptedAt: this.lastAcceptedAt
|
||||
}
|
||||
}
|
||||
|
||||
async createSubmission(input: {
|
||||
submittedByMemberId: string
|
||||
rawText: string
|
||||
sanitizedText: string | null
|
||||
moderationStatus: 'accepted' | 'posted' | 'rejected' | 'failed'
|
||||
moderationReason: string | null
|
||||
telegramChatId: string
|
||||
telegramMessageId: string
|
||||
telegramUpdateId: string
|
||||
}): Promise<{ submission: AnonymousFeedbackSubmissionRecord; duplicate: boolean }> {
|
||||
this.created.push({
|
||||
rawText: input.rawText,
|
||||
sanitizedText: input.sanitizedText,
|
||||
moderationStatus: input.moderationStatus,
|
||||
moderationReason: input.moderationReason
|
||||
})
|
||||
|
||||
return {
|
||||
submission: {
|
||||
id: 'submission-1',
|
||||
moderationStatus: input.moderationStatus
|
||||
},
|
||||
duplicate: this.duplicate
|
||||
}
|
||||
}
|
||||
|
||||
async markPosted(input: {
|
||||
submissionId: string
|
||||
postedChatId: string
|
||||
postedThreadId: string
|
||||
postedMessageId: string
|
||||
postedAt: Date
|
||||
}) {
|
||||
this.posted.push({
|
||||
submissionId: input.submissionId,
|
||||
postedThreadId: input.postedThreadId,
|
||||
postedMessageId: input.postedMessageId
|
||||
})
|
||||
}
|
||||
|
||||
async markFailed(submissionId: string, failureReason: string) {
|
||||
this.failed.push({ submissionId, failureReason })
|
||||
}
|
||||
}
|
||||
|
||||
describe('createAnonymousFeedbackService', () => {
|
||||
test('accepts and sanitizes a valid submission', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
const result = await service.submit({
|
||||
telegramUserId: '123',
|
||||
rawText: 'Please clean the kitchen tonight @roommate https://example.com',
|
||||
telegramChatId: 'chat-1',
|
||||
telegramMessageId: 'message-1',
|
||||
telegramUpdateId: 'update-1',
|
||||
now: new Date('2026-03-08T12:00:00.000Z')
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'accepted',
|
||||
submissionId: 'submission-1',
|
||||
sanitizedText: 'Please clean the kitchen tonight [mention removed] [link removed]'
|
||||
})
|
||||
expect(repository.created[0]).toMatchObject({
|
||||
moderationStatus: 'accepted'
|
||||
})
|
||||
})
|
||||
|
||||
test('rejects non-members before persistence', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
repository.member = null
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
const result = await service.submit({
|
||||
telegramUserId: '404',
|
||||
rawText: 'Please wash the dishes tonight',
|
||||
telegramChatId: 'chat-1',
|
||||
telegramMessageId: 'message-1',
|
||||
telegramUpdateId: 'update-1'
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'rejected',
|
||||
reason: 'not_member'
|
||||
})
|
||||
expect(repository.created).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('rejects blocklisted content and persists moderation outcome', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
const result = await service.submit({
|
||||
telegramUserId: '123',
|
||||
rawText: 'You are an idiot and this is disgusting',
|
||||
telegramChatId: 'chat-1',
|
||||
telegramMessageId: 'message-1',
|
||||
telegramUpdateId: 'update-1'
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'rejected',
|
||||
reason: 'blocklisted',
|
||||
detail: 'idiot'
|
||||
})
|
||||
expect(repository.created[0]).toMatchObject({
|
||||
moderationStatus: 'rejected',
|
||||
moderationReason: 'blocklisted:idiot'
|
||||
})
|
||||
})
|
||||
|
||||
test('enforces cooldown and daily cap', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
repository.lastAcceptedAt = new Date('2026-03-08T09:00:00.000Z')
|
||||
|
||||
const cooldownResult = await service.submit({
|
||||
telegramUserId: '123',
|
||||
rawText: 'Please take the trash out tonight',
|
||||
telegramChatId: 'chat-1',
|
||||
telegramMessageId: 'message-1',
|
||||
telegramUpdateId: 'update-1',
|
||||
now: new Date('2026-03-08T12:00:00.000Z')
|
||||
})
|
||||
|
||||
expect(cooldownResult).toEqual({
|
||||
status: 'rejected',
|
||||
reason: 'cooldown'
|
||||
})
|
||||
|
||||
repository.lastAcceptedAt = new Date('2026-03-07T00:00:00.000Z')
|
||||
repository.acceptedCountSince = 3
|
||||
|
||||
const dailyCapResult = await service.submit({
|
||||
telegramUserId: '123',
|
||||
rawText: 'Please ventilate the bathroom after showers',
|
||||
telegramChatId: 'chat-1',
|
||||
telegramMessageId: 'message-2',
|
||||
telegramUpdateId: 'update-2',
|
||||
now: new Date('2026-03-08T12:00:00.000Z')
|
||||
})
|
||||
|
||||
expect(dailyCapResult).toEqual({
|
||||
status: 'rejected',
|
||||
reason: 'daily_cap'
|
||||
})
|
||||
})
|
||||
|
||||
test('marks posted and failed submissions', async () => {
|
||||
const repository = new AnonymousFeedbackRepositoryStub()
|
||||
const service = createAnonymousFeedbackService(repository)
|
||||
|
||||
await service.markPosted({
|
||||
submissionId: 'submission-1',
|
||||
postedChatId: 'group-1',
|
||||
postedThreadId: 'thread-1',
|
||||
postedMessageId: 'post-1'
|
||||
})
|
||||
await service.markFailed('submission-2', 'telegram send failed')
|
||||
|
||||
expect(repository.posted).toEqual([
|
||||
{
|
||||
submissionId: 'submission-1',
|
||||
postedThreadId: 'thread-1',
|
||||
postedMessageId: 'post-1'
|
||||
}
|
||||
])
|
||||
expect(repository.failed).toEqual([
|
||||
{
|
||||
submissionId: 'submission-2',
|
||||
failureReason: 'telegram send failed'
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user