|
| 1 | +import chai from 'chai' |
| 2 | +import chaiAsPromised from 'chai-as-promised' |
| 3 | +import Sinon from 'sinon' |
| 4 | + |
| 5 | +chai.use(chaiAsPromised) |
| 6 | + |
| 7 | +const { expect } = chai |
| 8 | + |
| 9 | +import { Event } from '../../../../src/@types/event' |
| 10 | +import { IEventRepository, IReportRepository } from '../../../../src/@types/repositories' |
| 11 | +import { IEventStrategy } from '../../../../src/@types/message-handlers' |
| 12 | +import { IWebSocketAdapter } from '../../../../src/@types/adapters' |
| 13 | +import { IWotGraphService } from '../../../../src/@types/services' |
| 14 | +import { MessageType } from '../../../../src/@types/messages' |
| 15 | +import { ReportEventStrategy } from '../../../../src/handlers/event-strategies/report-event-strategy' |
| 16 | +import { ReportType } from '../../../../src/@types/report' |
| 17 | +import { Settings } from '../../../../src/@types/settings' |
| 18 | +import { WebSocketAdapterEvent } from '../../../../src/constants/adapter' |
| 19 | + |
| 20 | +describe('ReportEventStrategy', () => { |
| 21 | + const reporterPubkey = '2'.repeat(64) |
| 22 | + const reportedPubkey = '3'.repeat(64) |
| 23 | + |
| 24 | + const event: Event = { |
| 25 | + id: 'event-id', |
| 26 | + pubkey: reporterPubkey, |
| 27 | + kind: 1984, |
| 28 | + tags: [['p', reportedPubkey, 'spam']], |
| 29 | + } as any |
| 30 | + |
| 31 | + let webSocket: IWebSocketAdapter |
| 32 | + let eventRepository: IEventRepository |
| 33 | + let reportRepository: IReportRepository |
| 34 | + let wotGraphService: IWotGraphService |
| 35 | + let settings: () => Settings |
| 36 | + |
| 37 | + let webSocketEmitStub: Sinon.SinonStub |
| 38 | + let eventRepositoryCreateStub: Sinon.SinonStub |
| 39 | + let reportRepositoryCreateStub: Sinon.SinonStub |
| 40 | + let getDistanceStub: Sinon.SinonStub |
| 41 | + |
| 42 | + let strategy: IEventStrategy<Event, Promise<void>> |
| 43 | + |
| 44 | + let sandbox: Sinon.SinonSandbox |
| 45 | + |
| 46 | + beforeEach(() => { |
| 47 | + sandbox = Sinon.createSandbox() |
| 48 | + |
| 49 | + webSocketEmitStub = sandbox.stub() |
| 50 | + webSocket = { |
| 51 | + emit: webSocketEmitStub, |
| 52 | + } as any |
| 53 | + |
| 54 | + eventRepositoryCreateStub = sandbox.stub() |
| 55 | + eventRepository = { |
| 56 | + create: eventRepositoryCreateStub, |
| 57 | + } as any |
| 58 | + |
| 59 | + reportRepositoryCreateStub = sandbox.stub() |
| 60 | + reportRepository = { |
| 61 | + create: reportRepositoryCreateStub, |
| 62 | + } as any |
| 63 | + |
| 64 | + getDistanceStub = sandbox.stub() |
| 65 | + wotGraphService = { |
| 66 | + getDistance: getDistanceStub, |
| 67 | + } as any |
| 68 | + |
| 69 | + settings = () => ({ nip56: { enabled: true, trustedModerators: [] } }) as any |
| 70 | + |
| 71 | + strategy = new ReportEventStrategy(webSocket, eventRepository, reportRepository, wotGraphService, settings) |
| 72 | + }) |
| 73 | + |
| 74 | + afterEach(() => { |
| 75 | + sandbox.restore() |
| 76 | + }) |
| 77 | + |
| 78 | + describe('execute', () => { |
| 79 | + it('creates the event', async () => { |
| 80 | + eventRepositoryCreateStub.resolves(1) |
| 81 | + reportRepositoryCreateStub.resolves({}) |
| 82 | + getDistanceStub.resolves(1) |
| 83 | + |
| 84 | + await strategy.execute(event) |
| 85 | + |
| 86 | + expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event) |
| 87 | + }) |
| 88 | + |
| 89 | + it('broadcasts the event when newly created', async () => { |
| 90 | + eventRepositoryCreateStub.resolves(1) |
| 91 | + reportRepositoryCreateStub.resolves({}) |
| 92 | + getDistanceStub.resolves(1) |
| 93 | + |
| 94 | + await strategy.execute(event) |
| 95 | + |
| 96 | + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Message, [ |
| 97 | + MessageType.OK, |
| 98 | + 'event-id', |
| 99 | + true, |
| 100 | + '', |
| 101 | + ]) |
| 102 | + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Broadcast, event) |
| 103 | + }) |
| 104 | + |
| 105 | + it('does not broadcast or record a report when the event is a duplicate', async () => { |
| 106 | + eventRepositoryCreateStub.resolves(0) |
| 107 | + |
| 108 | + await strategy.execute(event) |
| 109 | + |
| 110 | + expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(WebSocketAdapterEvent.Message, [ |
| 111 | + MessageType.OK, |
| 112 | + 'event-id', |
| 113 | + true, |
| 114 | + 'duplicate:', |
| 115 | + ]) |
| 116 | + expect(reportRepositoryCreateStub).not.to.have.been.called |
| 117 | + }) |
| 118 | + |
| 119 | + it('records a report with full weight for a direct follow (distance 1)', async () => { |
| 120 | + eventRepositoryCreateStub.resolves(1) |
| 121 | + reportRepositoryCreateStub.resolves({}) |
| 122 | + getDistanceStub.resolves(1) |
| 123 | + |
| 124 | + await strategy.execute(event) |
| 125 | + |
| 126 | + expect(reportRepositoryCreateStub).to.have.been.calledOnceWithExactly({ |
| 127 | + id: 'event-id', |
| 128 | + reporterPubkey, |
| 129 | + reportedPubkey, |
| 130 | + reportedEventId: null, |
| 131 | + reportType: ReportType.SPAM, |
| 132 | + weight: 1, |
| 133 | + actionable: false, |
| 134 | + }) |
| 135 | + }) |
| 136 | + |
| 137 | + it('records a report with zero weight for a reporter outside the trust graph', async () => { |
| 138 | + eventRepositoryCreateStub.resolves(1) |
| 139 | + reportRepositoryCreateStub.resolves({}) |
| 140 | + getDistanceStub.resolves(undefined) |
| 141 | + |
| 142 | + await strategy.execute(event) |
| 143 | + |
| 144 | + expect(reportRepositoryCreateStub).to.have.been.calledOnceWithExactly({ |
| 145 | + id: 'event-id', |
| 146 | + reporterPubkey, |
| 147 | + reportedPubkey, |
| 148 | + reportedEventId: null, |
| 149 | + reportType: ReportType.SPAM, |
| 150 | + weight: 0, |
| 151 | + actionable: false, |
| 152 | + }) |
| 153 | + }) |
| 154 | + |
| 155 | + it('records an actionable, max-weight report from a trusted moderator regardless of distance', async () => { |
| 156 | + settings = () => ({ nip56: { enabled: true, trustedModerators: [reporterPubkey] } }) as any |
| 157 | + strategy = new ReportEventStrategy(webSocket, eventRepository, reportRepository, wotGraphService, settings) |
| 158 | + eventRepositoryCreateStub.resolves(1) |
| 159 | + reportRepositoryCreateStub.resolves({}) |
| 160 | + getDistanceStub.resolves(undefined) |
| 161 | + |
| 162 | + await strategy.execute(event) |
| 163 | + |
| 164 | + expect(reportRepositoryCreateStub).to.have.been.calledOnceWithExactly({ |
| 165 | + id: 'event-id', |
| 166 | + reporterPubkey, |
| 167 | + reportedPubkey, |
| 168 | + reportedEventId: null, |
| 169 | + reportType: ReportType.SPAM, |
| 170 | + weight: 1, |
| 171 | + actionable: true, |
| 172 | + }) |
| 173 | + }) |
| 174 | + |
| 175 | + it('stores the event but does not record a report when nip56 is disabled', async () => { |
| 176 | + settings = () => ({ nip56: { enabled: false, trustedModerators: [] } }) as any |
| 177 | + strategy = new ReportEventStrategy(webSocket, eventRepository, reportRepository, wotGraphService, settings) |
| 178 | + eventRepositoryCreateStub.resolves(1) |
| 179 | + |
| 180 | + await strategy.execute(event) |
| 181 | + |
| 182 | + expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event) |
| 183 | + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Broadcast, event) |
| 184 | + expect(reportRepositoryCreateStub).not.to.have.been.called |
| 185 | + expect(getDistanceStub).not.to.have.been.called |
| 186 | + }) |
| 187 | + |
| 188 | + it('does not reject the event when report recording fails', async () => { |
| 189 | + eventRepositoryCreateStub.resolves(1) |
| 190 | + getDistanceStub.resolves(1) |
| 191 | + reportRepositoryCreateStub.rejects(new Error('db unavailable')) |
| 192 | + |
| 193 | + await expect(strategy.execute(event)).to.eventually.be.fulfilled |
| 194 | + |
| 195 | + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Message, [ |
| 196 | + MessageType.OK, |
| 197 | + 'event-id', |
| 198 | + true, |
| 199 | + '', |
| 200 | + ]) |
| 201 | + }) |
| 202 | + |
| 203 | + it('rejects if unable to create the event', async () => { |
| 204 | + const error = new Error('event creation failed') |
| 205 | + eventRepositoryCreateStub.rejects(error) |
| 206 | + |
| 207 | + await expect(strategy.execute(event)).to.eventually.be.rejectedWith(error) |
| 208 | + |
| 209 | + expect(reportRepositoryCreateStub).not.to.have.been.called |
| 210 | + }) |
| 211 | + }) |
| 212 | +}) |
0 commit comments