A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
at main 10 kB view raw
1import { afterEach, describe, expect, it, vi } from "vitest"; 2import { 3 createAccountComment, 4 createAccountLabel, 5 createAccountReport, 6} from "../accountModeration.js"; 7import { 8 checkAccountThreshold, 9 loadThresholdConfigs, 10} from "../accountThreshold.js"; 11import { logger } from "../logger.js"; 12import { 13 accountLabelsThresholdAppliedCounter, 14 accountThresholdChecksCounter, 15 accountThresholdMetCounter, 16} from "../metrics.js"; 17import { 18 getPostLabelCountInWindow, 19 trackPostLabelForAccount, 20} from "../redis.js"; 21 22vi.mock("../logger.js", () => ({ 23 logger: { 24 info: vi.fn(), 25 warn: vi.fn(), 26 error: vi.fn(), 27 debug: vi.fn(), 28 }, 29})); 30 31vi.mock("../../rules/accountThreshold.js", () => ({ 32 ACCOUNT_THRESHOLD_CONFIGS: [ 33 { 34 labels: ["test-label"], 35 threshold: 3, 36 accountLabel: "test-account-label", 37 accountComment: "Test comment", 38 window: 5, 39 windowUnit: "days", 40 reportAcct: false, 41 commentAcct: false, 42 toLabel: true, 43 }, 44 { 45 labels: ["label-1", "label-2", "label-3"], 46 threshold: 5, 47 accountLabel: "multi-label-account", 48 accountComment: "Multi label comment", 49 window: 7, 50 windowUnit: "days", 51 reportAcct: true, 52 commentAcct: true, 53 toLabel: true, 54 }, 55 { 56 labels: "monitor-only-label", 57 threshold: 2, 58 accountLabel: "monitored", 59 accountComment: "Monitoring comment", 60 window: 3, 61 windowUnit: "days", 62 reportAcct: true, 63 commentAcct: false, 64 toLabel: false, 65 }, 66 { 67 labels: ["label-1", "shared-label"], 68 threshold: 2, 69 accountLabel: "shared-config", 70 accountComment: "Shared config comment", 71 window: 4, 72 windowUnit: "days", 73 reportAcct: false, 74 commentAcct: false, 75 toLabel: true, 76 }, 77 ], 78})); 79 80vi.mock("../redis.js", () => ({ 81 trackPostLabelForAccount: vi.fn(), 82 getPostLabelCountInWindow: vi.fn(), 83})); 84 85vi.mock("../accountModeration.js", () => ({ 86 createAccountLabel: vi.fn(), 87 createAccountReport: vi.fn(), 88 createAccountComment: vi.fn(), 89})); 90 91vi.mock("../metrics.js", () => ({ 92 accountLabelsThresholdAppliedCounter: { 93 inc: vi.fn(), 94 }, 95 accountThresholdChecksCounter: { 96 inc: vi.fn(), 97 }, 98 accountThresholdMetCounter: { 99 inc: vi.fn(), 100 }, 101})); 102 103describe("Account Threshold Logic", () => { 104 afterEach(() => { 105 vi.clearAllMocks(); 106 }); 107 108 describe("loadThresholdConfigs", () => { 109 it("should load and cache configs successfully", () => { 110 const configs = loadThresholdConfigs(); 111 expect(configs).toHaveLength(4); 112 expect(configs[0].labels).toEqual(["test-label"]); 113 expect(configs[1].labels).toEqual(["label-1", "label-2", "label-3"]); 114 }); 115 116 it("should return cached configs on subsequent calls", () => { 117 const configs1 = loadThresholdConfigs(); 118 const configs2 = loadThresholdConfigs(); 119 expect(configs1).toBe(configs2); 120 }); 121 }); 122 123 describe("checkAccountThreshold", () => { 124 const testDid = "did:plc:test123"; 125 const testUri = "at://did:plc:test123/app.bsky.feed.post/abc123"; 126 const testTimestamp = 1640000000000000; 127 128 it("should not check threshold for non-matching labels", async () => { 129 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 130 vi.mocked(getPostLabelCountInWindow).mockResolvedValue(0); 131 132 await checkAccountThreshold( 133 testDid, 134 testUri, 135 "non-matching-label", 136 testTimestamp, 137 ); 138 139 expect(trackPostLabelForAccount).not.toHaveBeenCalled(); 140 expect(getPostLabelCountInWindow).not.toHaveBeenCalled(); 141 }); 142 143 it("should track and check threshold for matching single label", async () => { 144 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 145 vi.mocked(getPostLabelCountInWindow).mockResolvedValue(2); 146 147 await checkAccountThreshold(testDid, testUri, "test-label", testTimestamp); 148 149 expect(accountThresholdChecksCounter.inc).toHaveBeenCalledWith({ 150 post_label: "test-label", 151 }); 152 expect(trackPostLabelForAccount).toHaveBeenCalledWith( 153 testDid, 154 "test-label", 155 testTimestamp, 156 5, 157 "days", 158 ); 159 expect(getPostLabelCountInWindow).toHaveBeenCalledWith( 160 testDid, 161 ["test-label"], 162 5, 163 "days", 164 testTimestamp, 165 ); 166 }); 167 168 it("should apply account label when threshold is met", async () => { 169 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 170 vi.mocked(getPostLabelCountInWindow).mockResolvedValue(3); 171 vi.mocked(createAccountLabel).mockResolvedValue(); 172 173 await checkAccountThreshold(testDid, testUri, "test-label", testTimestamp); 174 175 expect(accountThresholdMetCounter.inc).toHaveBeenCalledWith({ 176 account_label: "test-account-label", 177 }); 178 expect(createAccountLabel).toHaveBeenCalledWith( 179 testDid, 180 "test-account-label", 181 `Test comment\n\nThreshold: 3/3 in 5 days\n\nPost: ${testUri}\n\nPost Label: test-label`, 182 ); 183 expect(accountLabelsThresholdAppliedCounter.inc).toHaveBeenCalledWith({ 184 account_label: "test-account-label", 185 action: "label", 186 }); 187 }); 188 189 it("should not apply label when threshold not met", async () => { 190 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 191 vi.mocked(getPostLabelCountInWindow).mockResolvedValue(2); 192 193 await checkAccountThreshold(testDid, testUri, "test-label", testTimestamp); 194 195 expect(accountThresholdMetCounter.inc).not.toHaveBeenCalled(); 196 expect(createAccountLabel).not.toHaveBeenCalled(); 197 }); 198 199 it("should handle multi-label config with OR logic", async () => { 200 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 201 vi.mocked(getPostLabelCountInWindow).mockResolvedValue(5); 202 vi.mocked(createAccountLabel).mockResolvedValue(); 203 vi.mocked(createAccountReport).mockResolvedValue(); 204 vi.mocked(createAccountComment).mockResolvedValue(); 205 206 await checkAccountThreshold(testDid, testUri, "label-2", testTimestamp); 207 208 expect(getPostLabelCountInWindow).toHaveBeenCalledWith( 209 testDid, 210 ["label-1", "label-2", "label-3"], 211 7, 212 "days", 213 testTimestamp, 214 ); 215 expect(createAccountLabel).toHaveBeenCalledWith( 216 testDid, 217 "multi-label-account", 218 `Multi label comment\n\nThreshold: 5/5 in 7 days\n\nPost: ${testUri}\n\nPost Label: label-2`, 219 ); 220 expect(createAccountReport).toHaveBeenCalledWith( 221 testDid, 222 `Multi label comment\n\nThreshold: 5/5 in 7 days\n\nPost: ${testUri}\n\nPost Label: label-2`, 223 ); 224 expect(createAccountComment).toHaveBeenCalled(); 225 }); 226 227 it("should track but not label when toLabel is false", async () => { 228 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 229 vi.mocked(getPostLabelCountInWindow).mockResolvedValue(2); 230 vi.mocked(createAccountReport).mockResolvedValue(); 231 232 await checkAccountThreshold( 233 testDid, 234 testUri, 235 "monitor-only-label", 236 testTimestamp, 237 ); 238 239 expect(trackPostLabelForAccount).toHaveBeenCalled(); 240 expect(getPostLabelCountInWindow).toHaveBeenCalled(); 241 expect(createAccountLabel).not.toHaveBeenCalled(); 242 expect(createAccountReport).toHaveBeenCalled(); 243 expect(accountLabelsThresholdAppliedCounter.inc).toHaveBeenCalledWith({ 244 account_label: "monitored", 245 action: "report", 246 }); 247 }); 248 249 it("should increment all action metrics when threshold met", async () => { 250 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 251 vi.mocked(getPostLabelCountInWindow) 252 .mockResolvedValueOnce(5) 253 .mockResolvedValueOnce(1); 254 vi.mocked(createAccountLabel).mockResolvedValue(); 255 vi.mocked(createAccountReport).mockResolvedValue(); 256 vi.mocked(createAccountComment).mockResolvedValue(); 257 258 await checkAccountThreshold(testDid, testUri, "label-1", testTimestamp); 259 260 expect(accountLabelsThresholdAppliedCounter.inc).toHaveBeenCalledTimes(3); 261 expect(accountLabelsThresholdAppliedCounter.inc).toHaveBeenCalledWith({ 262 account_label: "multi-label-account", 263 action: "label", 264 }); 265 expect(accountLabelsThresholdAppliedCounter.inc).toHaveBeenCalledWith({ 266 account_label: "multi-label-account", 267 action: "report", 268 }); 269 expect(accountLabelsThresholdAppliedCounter.inc).toHaveBeenCalledWith({ 270 account_label: "multi-label-account", 271 action: "comment", 272 }); 273 }); 274 275 it("should handle Redis errors in trackPostLabelForAccount", async () => { 276 const redisError = new Error("Redis connection failed"); 277 vi.mocked(trackPostLabelForAccount).mockRejectedValue(redisError); 278 279 await expect( 280 checkAccountThreshold(testDid, testUri, "test-label", testTimestamp), 281 ).rejects.toThrow("Redis connection failed"); 282 283 expect(logger.error).toHaveBeenCalled(); 284 }); 285 286 it("should handle Redis errors in getPostLabelCountInWindow", async () => { 287 const redisError = new Error("Redis query failed"); 288 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 289 vi.mocked(getPostLabelCountInWindow).mockRejectedValue(redisError); 290 291 await expect( 292 checkAccountThreshold(testDid, testUri, "test-label", testTimestamp), 293 ).rejects.toThrow("Redis query failed"); 294 295 expect(logger.error).toHaveBeenCalled(); 296 }); 297 298 it("should handle multiple matching configs", async () => { 299 vi.mocked(trackPostLabelForAccount).mockResolvedValue(); 300 vi.mocked(getPostLabelCountInWindow) 301 .mockResolvedValueOnce(5) 302 .mockResolvedValueOnce(3); 303 vi.mocked(createAccountLabel).mockResolvedValue(); 304 vi.mocked(createAccountReport).mockResolvedValue(); 305 vi.mocked(createAccountComment).mockResolvedValue(); 306 307 await checkAccountThreshold(testDid, testUri, "label-1", testTimestamp); 308 309 expect(trackPostLabelForAccount).toHaveBeenCalledTimes(2); 310 expect(getPostLabelCountInWindow).toHaveBeenCalledTimes(2); 311 expect(createAccountLabel).toHaveBeenCalledTimes(2); 312 }); 313 }); 314});