A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules

feat: Add tests for countStarterPacks, checkPosts, profiles

This commit adds tests for the `countStarterPacks`, `checkPosts`, and
`checkProfiles` functions. These tests cover various scenarios,
including whitelisting, language filtering, URL resolution, and
moderation actions.

Skywatch 2b537122 4e4ec535

+1309
+231
src/rules/account/tests/countStarterPacks.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { countStarterPacks } from "../countStarterPacks.js"; 3 + 4 + // Mock dependencies 5 + vi.mock("../../../agent.js", () => ({ 6 + agent: { 7 + app: { 8 + bsky: { 9 + actor: { 10 + getProfile: vi.fn(), 11 + }, 12 + }, 13 + }, 14 + }, 15 + isLoggedIn: Promise.resolve(true), 16 + })); 17 + 18 + vi.mock("../../../logger.js", () => ({ 19 + logger: { 20 + info: vi.fn(), 21 + debug: vi.fn(), 22 + warn: vi.fn(), 23 + error: vi.fn(), 24 + }, 25 + })); 26 + 27 + vi.mock("../../../moderation.js", () => ({ 28 + createAccountLabel: vi.fn(), 29 + })); 30 + 31 + vi.mock("../../../limits.js", () => ({ 32 + limit: vi.fn((fn) => fn()), 33 + })); 34 + 35 + import { agent } from "../../../agent.js"; 36 + import { logger } from "../../../logger.js"; 37 + import { createAccountLabel } from "../../../moderation.js"; 38 + import { limit } from "../../../limits.js"; 39 + 40 + describe("countStarterPacks", () => { 41 + beforeEach(() => { 42 + vi.clearAllMocks(); 43 + }); 44 + 45 + it("should skip whitelisted DIDs", async () => { 46 + const whitelistedDid = "did:plc:gpunjjgvlyb4racypz3yfiq4"; 47 + const time = Date.now() * 1000; 48 + 49 + await countStarterPacks(whitelistedDid, time); 50 + 51 + expect(logger.debug).toHaveBeenCalledWith( 52 + { process: "COUNTSTARTERPACKS", did: whitelistedDid, time }, 53 + "Account is whitelisted", 54 + ); 55 + expect(agent.app.bsky.actor.getProfile).not.toHaveBeenCalled(); 56 + expect(createAccountLabel).not.toHaveBeenCalled(); 57 + }); 58 + 59 + it("should label accounts with more than 20 starter packs", async () => { 60 + const did = "did:plc:test123"; 61 + const time = Date.now() * 1000; 62 + const starterPackCount = 25; 63 + 64 + vi.mocked(agent.app.bsky.actor.getProfile).mockResolvedValue({ 65 + data: { 66 + associated: { 67 + starterPacks: starterPackCount, 68 + }, 69 + }, 70 + } as any); 71 + 72 + await countStarterPacks(did, time); 73 + 74 + expect(limit).toHaveBeenCalled(); 75 + expect(agent.app.bsky.actor.getProfile).toHaveBeenCalledWith({ 76 + actor: did, 77 + }); 78 + expect(logger.info).toHaveBeenCalledWith( 79 + { 80 + process: "COUNTSTARTERPACKS", 81 + did, 82 + time, 83 + starterPackCount, 84 + }, 85 + "Labeling account with excessive starter packs", 86 + ); 87 + expect(createAccountLabel).toHaveBeenCalledWith( 88 + did, 89 + "follow-farming", 90 + `${time}: Account has ${starterPackCount} starter packs`, 91 + ); 92 + }); 93 + 94 + it("should not label accounts with exactly 20 starter packs", async () => { 95 + const did = "did:plc:test456"; 96 + const time = Date.now() * 1000; 97 + 98 + vi.mocked(agent.app.bsky.actor.getProfile).mockResolvedValue({ 99 + data: { 100 + associated: { 101 + starterPacks: 20, 102 + }, 103 + }, 104 + } as any); 105 + 106 + await countStarterPacks(did, time); 107 + 108 + expect(agent.app.bsky.actor.getProfile).toHaveBeenCalledWith({ 109 + actor: did, 110 + }); 111 + expect(createAccountLabel).not.toHaveBeenCalled(); 112 + }); 113 + 114 + it("should not label accounts with fewer than 20 starter packs", async () => { 115 + const did = "did:plc:test789"; 116 + const time = Date.now() * 1000; 117 + 118 + vi.mocked(agent.app.bsky.actor.getProfile).mockResolvedValue({ 119 + data: { 120 + associated: { 121 + starterPacks: 15, 122 + }, 123 + }, 124 + } as any); 125 + 126 + await countStarterPacks(did, time); 127 + 128 + expect(agent.app.bsky.actor.getProfile).toHaveBeenCalledWith({ 129 + actor: did, 130 + }); 131 + expect(createAccountLabel).not.toHaveBeenCalled(); 132 + }); 133 + 134 + it("should not label accounts with no associated starter packs", async () => { 135 + const did = "did:plc:test000"; 136 + const time = Date.now() * 1000; 137 + 138 + vi.mocked(agent.app.bsky.actor.getProfile).mockResolvedValue({ 139 + data: { 140 + associated: undefined, 141 + }, 142 + } as any); 143 + 144 + await countStarterPacks(did, time); 145 + 146 + expect(agent.app.bsky.actor.getProfile).toHaveBeenCalledWith({ 147 + actor: did, 148 + }); 149 + expect(createAccountLabel).not.toHaveBeenCalled(); 150 + }); 151 + 152 + it("should not label accounts with no starter packs field", async () => { 153 + const did = "did:plc:test111"; 154 + const time = Date.now() * 1000; 155 + 156 + vi.mocked(agent.app.bsky.actor.getProfile).mockResolvedValue({ 157 + data: { 158 + associated: { 159 + starterPacks: undefined, 160 + }, 161 + }, 162 + } as any); 163 + 164 + await countStarterPacks(did, time); 165 + 166 + expect(agent.app.bsky.actor.getProfile).toHaveBeenCalledWith({ 167 + actor: did, 168 + }); 169 + expect(createAccountLabel).not.toHaveBeenCalled(); 170 + }); 171 + 172 + it("should handle errors when fetching profile", async () => { 173 + const did = "did:plc:testerror"; 174 + const time = Date.now() * 1000; 175 + const error = new Error("Profile not found"); 176 + 177 + vi.mocked(agent.app.bsky.actor.getProfile).mockRejectedValue(error); 178 + 179 + await countStarterPacks(did, time); 180 + 181 + expect(logger.error).toHaveBeenCalledWith( 182 + { 183 + process: "COUNTSTARTERPACKS", 184 + did, 185 + time, 186 + name: error.name, 187 + message: error.message, 188 + }, 189 + "Error checking associated accounts", 190 + ); 191 + expect(createAccountLabel).not.toHaveBeenCalled(); 192 + }); 193 + 194 + it("should handle non-Error exceptions", async () => { 195 + const did = "did:plc:teststring"; 196 + const time = Date.now() * 1000; 197 + const error = "String error message"; 198 + 199 + vi.mocked(agent.app.bsky.actor.getProfile).mockRejectedValue(error); 200 + 201 + await countStarterPacks(did, time); 202 + 203 + expect(logger.error).toHaveBeenCalledWith( 204 + { 205 + process: "COUNTSTARTERPACKS", 206 + did, 207 + time, 208 + error: String(error), 209 + }, 210 + "Error checking associated accounts", 211 + ); 212 + expect(createAccountLabel).not.toHaveBeenCalled(); 213 + }); 214 + 215 + it("should use limit function for rate limiting", async () => { 216 + const did = "did:plc:testlimit"; 217 + const time = Date.now() * 1000; 218 + 219 + vi.mocked(agent.app.bsky.actor.getProfile).mockResolvedValue({ 220 + data: { 221 + associated: { 222 + starterPacks: 10, 223 + }, 224 + }, 225 + } as any); 226 + 227 + await countStarterPacks(did, time); 228 + 229 + expect(limit).toHaveBeenCalledWith(expect.any(Function)); 230 + }); 231 + });
+446
src/rules/posts/tests/checkPosts.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { checkPosts } from "../checkPosts.js"; 3 + import { Post } from "../../../types.js"; 4 + 5 + // Mock dependencies 6 + vi.mock("../constants.js", () => ({ 7 + LINK_SHORTENER: /tinyurl\.com|bit\.ly/i, 8 + POST_CHECKS: [ 9 + { 10 + label: "test-label", 11 + comment: "Test comment", 12 + check: /spam|scam/i, 13 + toLabel: true, 14 + reportPost: false, 15 + reportAcct: false, 16 + commentAcct: false, 17 + }, 18 + { 19 + label: "language-specific", 20 + comment: "English only test", 21 + language: ["eng"], 22 + check: /hello/i, 23 + toLabel: true, 24 + reportPost: false, 25 + reportAcct: false, 26 + commentAcct: false, 27 + }, 28 + { 29 + label: "whitelisted-test", 30 + comment: "Has whitelist", 31 + check: /bad/i, 32 + whitelist: /good bad/i, 33 + toLabel: true, 34 + reportPost: false, 35 + reportAcct: false, 36 + commentAcct: false, 37 + }, 38 + { 39 + label: "ignored-did", 40 + comment: "Ignored DID test", 41 + check: /test/i, 42 + ignoredDIDs: ["did:plc:ignored123"], 43 + toLabel: true, 44 + reportPost: false, 45 + reportAcct: false, 46 + commentAcct: false, 47 + }, 48 + { 49 + label: "all-actions", 50 + comment: "All actions enabled", 51 + check: /report/i, 52 + toLabel: true, 53 + reportPost: true, 54 + reportAcct: true, 55 + commentAcct: true, 56 + }, 57 + ], 58 + })); 59 + 60 + vi.mock("../../../logger.js", () => ({ 61 + logger: { 62 + info: vi.fn(), 63 + debug: vi.fn(), 64 + warn: vi.fn(), 65 + error: vi.fn(), 66 + }, 67 + })); 68 + 69 + vi.mock("../../account/countStarterPacks.js", () => ({ 70 + countStarterPacks: vi.fn(), 71 + })); 72 + 73 + vi.mock("../../../moderation.js", () => ({ 74 + createPostLabel: vi.fn(), 75 + createAccountReport: vi.fn(), 76 + createAccountComment: vi.fn(), 77 + createPostReport: vi.fn(), 78 + })); 79 + 80 + vi.mock("../../../utils/getLanguage.js", () => ({ 81 + getLanguage: vi.fn().mockResolvedValue("eng"), 82 + })); 83 + 84 + vi.mock("../../../utils/getFinalUrl.js", () => ({ 85 + getFinalUrl: vi.fn(), 86 + })); 87 + 88 + vi.mock("../../../constants.js", () => ({ 89 + GLOBAL_ALLOW: ["did:plc:globalallow"], 90 + })); 91 + 92 + import { logger } from "../../../logger.js"; 93 + import { countStarterPacks } from "../../account/countStarterPacks.js"; 94 + import { 95 + createPostLabel, 96 + createAccountReport, 97 + createAccountComment, 98 + createPostReport, 99 + } from "../../../moderation.js"; 100 + import { getLanguage } from "../../../utils/getLanguage.js"; 101 + import { getFinalUrl } from "../../../utils/getFinalUrl.js"; 102 + 103 + describe("checkPosts", () => { 104 + beforeEach(() => { 105 + vi.clearAllMocks(); 106 + }); 107 + 108 + const createMockPost = (overrides?: Partial<Post>): Post[] => [ 109 + { 110 + did: "did:plc:test123", 111 + time: Date.now() * 1000, 112 + rkey: "test-rkey", 113 + atURI: "at://did:plc:test123/app.bsky.feed.post/test-rkey", 114 + text: "This is a test post", 115 + cid: "test-cid", 116 + ...overrides, 117 + }, 118 + ]; 119 + 120 + describe("Global allow list", () => { 121 + it("should skip posts from globally allowed DIDs", async () => { 122 + const post = createMockPost({ did: "did:plc:globalallow" }); 123 + 124 + await checkPosts(post); 125 + 126 + expect(logger.warn).toHaveBeenCalledWith( 127 + { 128 + process: "CHECKPOSTS", 129 + did: "did:plc:globalallow", 130 + atURI: post[0].atURI, 131 + }, 132 + "Global AllowListed DID", 133 + ); 134 + expect(createPostLabel).not.toHaveBeenCalled(); 135 + }); 136 + 137 + it("should process posts from non-globally-allowed DIDs", async () => { 138 + const post = createMockPost({ text: "spam post" }); 139 + 140 + await checkPosts(post); 141 + 142 + expect(logger.warn).not.toHaveBeenCalledWith( 143 + expect.objectContaining({ did: post[0].did }), 144 + "Global AllowListed DID", 145 + ); 146 + }); 147 + }); 148 + 149 + describe("URL shortener resolution", () => { 150 + it("should resolve shortened URLs", async () => { 151 + const post = createMockPost({ text: "Check this out https://tinyurl.com/test123" }); 152 + vi.mocked(getFinalUrl).mockResolvedValue("https://example.com/full-url"); 153 + 154 + await checkPosts(post); 155 + 156 + expect(logger.debug).toHaveBeenCalledWith( 157 + { 158 + process: "CHECKPOSTS", 159 + url: "https://tinyurl.com/test123", 160 + did: post[0].did, 161 + }, 162 + "Resolving shortened URL", 163 + ); 164 + expect(getFinalUrl).toHaveBeenCalledWith("https://tinyurl.com/test123"); 165 + expect(logger.debug).toHaveBeenCalledWith( 166 + { 167 + process: "CHECKPOSTS", 168 + originalUrl: "https://tinyurl.com/test123", 169 + resolvedUrl: "https://example.com/full-url", 170 + did: post[0].did, 171 + }, 172 + "Shortened URL resolved", 173 + ); 174 + expect(post[0].text).toBe("Check this out https://example.com/full-url"); 175 + }); 176 + 177 + it("should not replace URL if resolution returns same URL", async () => { 178 + const post = createMockPost({ text: "Check https://tinyurl.com/test" }); 179 + vi.mocked(getFinalUrl).mockResolvedValue("https://tinyurl.com/test"); 180 + 181 + await checkPosts(post); 182 + 183 + expect(getFinalUrl).toHaveBeenCalled(); 184 + expect(post[0].text).toBe("Check https://tinyurl.com/test"); 185 + expect(logger.debug).not.toHaveBeenCalledWith( 186 + expect.objectContaining({ originalUrl: expect.anything() }), 187 + "Shortened URL resolved", 188 + ); 189 + }); 190 + 191 + it("should handle URL resolution errors gracefully", async () => { 192 + const post = createMockPost({ text: "https://tinyurl.com/broken" }); 193 + const error = new Error("Network timeout"); 194 + vi.mocked(getFinalUrl).mockRejectedValue(error); 195 + 196 + await checkPosts(post); 197 + 198 + expect(logger.error).toHaveBeenCalledWith( 199 + { 200 + process: "CHECKPOSTS", 201 + text: post[0].text, 202 + did: post[0].did, 203 + atURI: post[0].atURI, 204 + name: error.name, 205 + message: error.message, 206 + }, 207 + "Failed to resolve shortened URL", 208 + ); 209 + expect(post[0].text).toBe("https://tinyurl.com/broken"); 210 + }); 211 + 212 + it("should handle non-Error exceptions during URL resolution", async () => { 213 + const post = createMockPost({ text: "https://bit.ly/test" }); 214 + const error = "String error"; 215 + vi.mocked(getFinalUrl).mockRejectedValue(error); 216 + 217 + await checkPosts(post); 218 + 219 + expect(logger.error).toHaveBeenCalledWith( 220 + { 221 + process: "CHECKPOSTS", 222 + text: post[0].text, 223 + did: post[0].did, 224 + atURI: post[0].atURI, 225 + error: String(error), 226 + }, 227 + "Failed to resolve shortened URL", 228 + ); 229 + }); 230 + 231 + it("should not attempt to resolve non-shortened URLs", async () => { 232 + const post = createMockPost({ text: "Normal https://example.com URL" }); 233 + 234 + await checkPosts(post); 235 + 236 + expect(getFinalUrl).not.toHaveBeenCalled(); 237 + }); 238 + }); 239 + 240 + describe("Pattern matching and labeling", () => { 241 + it("should label posts matching check patterns", async () => { 242 + const post = createMockPost({ text: "This is spam content" }); 243 + 244 + await checkPosts(post); 245 + 246 + expect(logger.info).toHaveBeenCalledWith( 247 + { 248 + process: "CHECKPOSTS", 249 + label: "test-label", 250 + did: post[0].did, 251 + atURI: post[0].atURI, 252 + }, 253 + "Labeling post", 254 + ); 255 + expect(createPostLabel).toHaveBeenCalledWith( 256 + post[0].atURI, 257 + post[0].cid, 258 + "test-label", 259 + expect.stringContaining("Test comment"), 260 + undefined, 261 + ); 262 + }); 263 + 264 + it("should not label posts that don't match patterns", async () => { 265 + const post = createMockPost({ text: "Totally normal content" }); 266 + 267 + await checkPosts(post); 268 + 269 + expect(createPostLabel).not.toHaveBeenCalled(); 270 + }); 271 + 272 + it("should call countStarterPacks when pattern matches", async () => { 273 + const post = createMockPost({ text: "spam message" }); 274 + 275 + await checkPosts(post); 276 + 277 + expect(countStarterPacks).toHaveBeenCalledWith(post[0].did, post[0].time); 278 + }); 279 + }); 280 + 281 + describe("Language filtering", () => { 282 + it("should only check language-specific patterns for matching languages", async () => { 283 + const post = createMockPost({ text: "hello world" }); 284 + vi.mocked(getLanguage).mockResolvedValue("eng"); 285 + 286 + await checkPosts(post); 287 + 288 + expect(createPostLabel).toHaveBeenCalledWith( 289 + post[0].atURI, 290 + post[0].cid, 291 + "language-specific", 292 + expect.any(String), 293 + undefined, 294 + ); 295 + }); 296 + 297 + it("should skip language-specific patterns for non-matching languages", async () => { 298 + const post = createMockPost({ text: "hello world" }); 299 + vi.mocked(getLanguage).mockResolvedValue("spa"); 300 + 301 + await checkPosts(post); 302 + 303 + expect(createPostLabel).not.toHaveBeenCalledWith( 304 + expect.any(String), 305 + expect.any(String), 306 + "language-specific", 307 + expect.any(String), 308 + undefined, 309 + ); 310 + }); 311 + }); 312 + 313 + describe("Whitelist handling", () => { 314 + it("should skip patterns when whitelist matches", async () => { 315 + const post = createMockPost({ text: "this is good bad content" }); 316 + 317 + await checkPosts(post); 318 + 319 + expect(logger.debug).toHaveBeenCalledWith( 320 + { 321 + process: "CHECKPOSTS", 322 + did: post[0].did, 323 + atURI: post[0].atURI, 324 + }, 325 + "Whitelisted phrase found", 326 + ); 327 + expect(createPostLabel).not.toHaveBeenCalledWith( 328 + expect.any(String), 329 + expect.any(String), 330 + "whitelisted-test", 331 + expect.any(String), 332 + undefined, 333 + ); 334 + }); 335 + 336 + it("should label when pattern matches but whitelist doesn't", async () => { 337 + const post = createMockPost({ text: "just bad content" }); 338 + 339 + await checkPosts(post); 340 + 341 + expect(createPostLabel).toHaveBeenCalledWith( 342 + post[0].atURI, 343 + post[0].cid, 344 + "whitelisted-test", 345 + expect.any(String), 346 + undefined, 347 + ); 348 + }); 349 + }); 350 + 351 + describe("Ignored DIDs", () => { 352 + it("should skip checks for ignored DIDs", async () => { 353 + const post = createMockPost({ 354 + did: "did:plc:ignored123", 355 + text: "test content", 356 + }); 357 + 358 + await checkPosts(post); 359 + 360 + expect(logger.debug).toHaveBeenCalledWith( 361 + { 362 + process: "CHECKPOSTS", 363 + did: "did:plc:ignored123", 364 + atURI: post[0].atURI, 365 + }, 366 + "Whitelisted DID", 367 + ); 368 + expect(createPostLabel).not.toHaveBeenCalledWith( 369 + expect.any(String), 370 + expect.any(String), 371 + "ignored-did", 372 + expect.any(String), 373 + undefined, 374 + ); 375 + }); 376 + 377 + it("should check non-ignored DIDs", async () => { 378 + const post = createMockPost({ 379 + did: "did:plc:notignored", 380 + text: "test content", 381 + }); 382 + 383 + await checkPosts(post); 384 + 385 + expect(createPostLabel).toHaveBeenCalledWith( 386 + post[0].atURI, 387 + post[0].cid, 388 + "ignored-did", 389 + expect.any(String), 390 + undefined, 391 + ); 392 + }); 393 + }); 394 + 395 + describe("Moderation actions", () => { 396 + it("should execute all moderation actions when enabled", async () => { 397 + const post = createMockPost({ text: "report this content" }); 398 + 399 + await checkPosts(post); 400 + 401 + expect(createPostLabel).toHaveBeenCalledWith( 402 + post[0].atURI, 403 + post[0].cid, 404 + "all-actions", 405 + expect.any(String), 406 + undefined, 407 + ); 408 + expect(createPostReport).toHaveBeenCalledWith( 409 + post[0].atURI, 410 + post[0].cid, 411 + expect.any(String), 412 + ); 413 + expect(createAccountReport).toHaveBeenCalledWith( 414 + post[0].did, 415 + expect.any(String), 416 + ); 417 + expect(createAccountComment).toHaveBeenCalledWith( 418 + post[0].did, 419 + expect.any(String), 420 + ); 421 + }); 422 + 423 + it("should log all moderation actions", async () => { 424 + const post = createMockPost({ text: "report this" }); 425 + 426 + await checkPosts(post); 427 + 428 + expect(logger.info).toHaveBeenCalledWith( 429 + expect.objectContaining({ label: "all-actions" }), 430 + "Labeling post", 431 + ); 432 + expect(logger.info).toHaveBeenCalledWith( 433 + expect.objectContaining({ label: "all-actions" }), 434 + "Reporting post", 435 + ); 436 + expect(logger.info).toHaveBeenCalledWith( 437 + expect.objectContaining({ label: "all-actions" }), 438 + "Reporting account", 439 + ); 440 + expect(logger.info).toHaveBeenCalledWith( 441 + expect.objectContaining({ label: "all-actions" }), 442 + "Commenting on account", 443 + ); 444 + }); 445 + }); 446 + });
+632
src/rules/profiles/tests/checkProfiles.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { checkDescription, checkDisplayName } from "../checkProfiles.js"; 3 + 4 + // Mock dependencies 5 + vi.mock("../constants.js", () => ({ 6 + PROFILE_CHECKS: [ 7 + { 8 + label: "test-description", 9 + comment: "Test description check", 10 + description: true, 11 + displayName: false, 12 + check: /spam|scam/i, 13 + toLabel: true, 14 + reportAcct: false, 15 + commentAcct: false, 16 + }, 17 + { 18 + label: "test-displayname", 19 + comment: "Test display name check", 20 + description: false, 21 + displayName: true, 22 + check: /fake|bot/i, 23 + toLabel: true, 24 + reportAcct: false, 25 + commentAcct: false, 26 + }, 27 + { 28 + label: "language-specific", 29 + comment: "English only test", 30 + language: ["eng"], 31 + description: true, 32 + displayName: false, 33 + check: /hello/i, 34 + toLabel: true, 35 + reportAcct: false, 36 + commentAcct: false, 37 + }, 38 + { 39 + label: "whitelisted-test", 40 + comment: "Has whitelist", 41 + description: true, 42 + displayName: false, 43 + check: /bad/i, 44 + whitelist: /good bad/i, 45 + toLabel: true, 46 + reportAcct: false, 47 + commentAcct: false, 48 + }, 49 + { 50 + label: "ignored-did", 51 + comment: "Ignored DID test", 52 + description: true, 53 + displayName: false, 54 + check: /test/i, 55 + ignoredDIDs: ["did:plc:ignored123"], 56 + toLabel: true, 57 + reportAcct: false, 58 + commentAcct: false, 59 + }, 60 + { 61 + label: "all-actions", 62 + comment: "All actions enabled", 63 + description: true, 64 + displayName: false, 65 + check: /report/i, 66 + toLabel: true, 67 + reportAcct: true, 68 + commentAcct: true, 69 + }, 70 + { 71 + label: "both-fields", 72 + comment: "Check both description and displayName", 73 + description: true, 74 + displayName: true, 75 + check: /suspicious/i, 76 + toLabel: true, 77 + reportAcct: false, 78 + commentAcct: false, 79 + }, 80 + ], 81 + })); 82 + 83 + vi.mock("../../../logger.js", () => ({ 84 + logger: { 85 + info: vi.fn(), 86 + debug: vi.fn(), 87 + warn: vi.fn(), 88 + error: vi.fn(), 89 + }, 90 + })); 91 + 92 + vi.mock("../../../moderation.js", () => ({ 93 + createAccountLabel: vi.fn(), 94 + createAccountReport: vi.fn(), 95 + createAccountComment: vi.fn(), 96 + })); 97 + 98 + vi.mock("../../../utils/getLanguage.js", () => ({ 99 + getLanguage: vi.fn().mockResolvedValue("eng"), 100 + })); 101 + 102 + vi.mock("../../../constants.js", () => ({ 103 + GLOBAL_ALLOW: ["did:plc:globalallow"], 104 + })); 105 + 106 + import { logger } from "../../../logger.js"; 107 + import { 108 + createAccountLabel, 109 + createAccountReport, 110 + createAccountComment, 111 + } from "../../../moderation.js"; 112 + import { getLanguage } from "../../../utils/getLanguage.js"; 113 + 114 + describe("checkProfiles", () => { 115 + beforeEach(() => { 116 + vi.clearAllMocks(); 117 + }); 118 + 119 + const mockDid = "did:plc:test123"; 120 + const mockTime = Date.now() * 1000; 121 + const mockDisplayName = "Test User"; 122 + const mockDescription = "This is a test description"; 123 + 124 + describe("checkDescription", () => { 125 + describe("Global allow list", () => { 126 + it("should skip profiles from globally allowed DIDs", async () => { 127 + await checkDescription( 128 + "did:plc:globalallow", 129 + mockTime, 130 + mockDisplayName, 131 + mockDescription, 132 + ); 133 + 134 + expect(logger.warn).toHaveBeenCalledWith( 135 + { 136 + process: "CHECKDESCRIPTION", 137 + did: "did:plc:globalallow", 138 + time: mockTime, 139 + displayName: mockDisplayName, 140 + description: mockDescription, 141 + }, 142 + "Global AllowListed DID", 143 + ); 144 + expect(createAccountLabel).not.toHaveBeenCalled(); 145 + }); 146 + 147 + it("should process non-globally-allowed DIDs", async () => { 148 + await checkDescription( 149 + mockDid, 150 + mockTime, 151 + mockDisplayName, 152 + "spam content", 153 + ); 154 + 155 + expect(logger.warn).not.toHaveBeenCalledWith( 156 + expect.objectContaining({ did: mockDid }), 157 + "Global AllowListed DID", 158 + ); 159 + }); 160 + }); 161 + 162 + describe("Pattern matching and labeling", () => { 163 + it("should label profiles with matching descriptions", async () => { 164 + await checkDescription( 165 + mockDid, 166 + mockTime, 167 + mockDisplayName, 168 + "This is spam content", 169 + ); 170 + 171 + expect(logger.info).toHaveBeenCalledWith( 172 + { 173 + process: "CHECKDESCRIPTION", 174 + did: mockDid, 175 + time: mockTime, 176 + displayName: mockDisplayName, 177 + description: "This is spam content", 178 + label: "test-description", 179 + }, 180 + "Labeling account", 181 + ); 182 + expect(createAccountLabel).toHaveBeenCalledWith( 183 + mockDid, 184 + "test-description", 185 + expect.stringContaining("Test description check"), 186 + ); 187 + }); 188 + 189 + it("should not label profiles without matching descriptions", async () => { 190 + await checkDescription( 191 + mockDid, 192 + mockTime, 193 + mockDisplayName, 194 + "Normal content", 195 + ); 196 + 197 + expect(createAccountLabel).not.toHaveBeenCalledWith( 198 + mockDid, 199 + "test-description", 200 + expect.any(String), 201 + ); 202 + }); 203 + 204 + it("should not check description when description field is false", async () => { 205 + await checkDescription( 206 + mockDid, 207 + mockTime, 208 + "fake account", 209 + mockDescription, 210 + ); 211 + 212 + // test-displayname has displayName: true, description: false 213 + // so it should not trigger on description content 214 + expect(createAccountLabel).not.toHaveBeenCalledWith( 215 + mockDid, 216 + "test-displayname", 217 + expect.any(String), 218 + ); 219 + }); 220 + 221 + it("should handle empty description", async () => { 222 + await checkDescription(mockDid, mockTime, mockDisplayName, ""); 223 + 224 + expect(createAccountLabel).not.toHaveBeenCalled(); 225 + }); 226 + }); 227 + 228 + describe("Language filtering", () => { 229 + it("should check language-specific patterns for matching languages", async () => { 230 + vi.mocked(getLanguage).mockResolvedValue("eng"); 231 + 232 + await checkDescription( 233 + mockDid, 234 + mockTime, 235 + mockDisplayName, 236 + "hello world", 237 + ); 238 + 239 + expect(createAccountLabel).toHaveBeenCalledWith( 240 + mockDid, 241 + "language-specific", 242 + expect.any(String), 243 + ); 244 + }); 245 + 246 + it("should skip language-specific patterns for non-matching languages", async () => { 247 + vi.mocked(getLanguage).mockResolvedValue("spa"); 248 + 249 + await checkDescription( 250 + mockDid, 251 + mockTime, 252 + mockDisplayName, 253 + "hello world", 254 + ); 255 + 256 + expect(createAccountLabel).not.toHaveBeenCalledWith( 257 + mockDid, 258 + "language-specific", 259 + expect.any(String), 260 + ); 261 + }); 262 + }); 263 + 264 + describe("Whitelist handling", () => { 265 + it("should skip patterns when whitelist matches", async () => { 266 + await checkDescription( 267 + mockDid, 268 + mockTime, 269 + mockDisplayName, 270 + "this is good bad content", 271 + ); 272 + 273 + expect(logger.debug).toHaveBeenCalledWith( 274 + { 275 + process: "CHECKDESCRIPTION", 276 + did: mockDid, 277 + time: mockTime, 278 + displayName: mockDisplayName, 279 + description: "this is good bad content", 280 + }, 281 + "Whitelisted phrase found", 282 + ); 283 + expect(createAccountLabel).not.toHaveBeenCalledWith( 284 + mockDid, 285 + "whitelisted-test", 286 + expect.any(String), 287 + ); 288 + }); 289 + 290 + it("should label when pattern matches but whitelist doesn't", async () => { 291 + await checkDescription( 292 + mockDid, 293 + mockTime, 294 + mockDisplayName, 295 + "just bad content", 296 + ); 297 + 298 + expect(createAccountLabel).toHaveBeenCalledWith( 299 + mockDid, 300 + "whitelisted-test", 301 + expect.any(String), 302 + ); 303 + }); 304 + }); 305 + 306 + describe("Ignored DIDs", () => { 307 + it("should skip checks for ignored DIDs", async () => { 308 + await checkDescription( 309 + "did:plc:ignored123", 310 + mockTime, 311 + mockDisplayName, 312 + "test content", 313 + ); 314 + 315 + expect(logger.debug).toHaveBeenCalledWith( 316 + { 317 + process: "CHECKDESCRIPTION", 318 + did: "did:plc:ignored123", 319 + time: mockTime, 320 + displayName: mockDisplayName, 321 + description: "test content", 322 + }, 323 + "Whitelisted DID", 324 + ); 325 + expect(createAccountLabel).not.toHaveBeenCalledWith( 326 + "did:plc:ignored123", 327 + "ignored-did", 328 + expect.any(String), 329 + ); 330 + }); 331 + 332 + it("should check non-ignored DIDs", async () => { 333 + await checkDescription( 334 + mockDid, 335 + mockTime, 336 + mockDisplayName, 337 + "test content", 338 + ); 339 + 340 + expect(createAccountLabel).toHaveBeenCalledWith( 341 + mockDid, 342 + "ignored-did", 343 + expect.any(String), 344 + ); 345 + }); 346 + }); 347 + 348 + describe("Moderation actions", () => { 349 + it("should execute all moderation actions when enabled", async () => { 350 + await checkDescription( 351 + mockDid, 352 + mockTime, 353 + mockDisplayName, 354 + "report this content", 355 + ); 356 + 357 + expect(createAccountLabel).toHaveBeenCalledWith( 358 + mockDid, 359 + "all-actions", 360 + expect.any(String), 361 + ); 362 + expect(createAccountReport).toHaveBeenCalledWith( 363 + mockDid, 364 + expect.any(String), 365 + ); 366 + expect(createAccountComment).toHaveBeenCalledWith( 367 + mockDid, 368 + expect.any(String), 369 + ); 370 + }); 371 + 372 + it("should log all moderation actions", async () => { 373 + await checkDescription( 374 + mockDid, 375 + mockTime, 376 + mockDisplayName, 377 + "report this", 378 + ); 379 + 380 + expect(logger.info).toHaveBeenCalledWith( 381 + expect.objectContaining({ label: "all-actions" }), 382 + "Labeling account", 383 + ); 384 + expect(logger.info).toHaveBeenCalledWith( 385 + expect.objectContaining({ label: "all-actions" }), 386 + "Reporting account", 387 + ); 388 + expect(logger.info).toHaveBeenCalledWith( 389 + expect.objectContaining({ label: "all-actions" }), 390 + "Commenting on account", 391 + ); 392 + }); 393 + }); 394 + }); 395 + 396 + describe("checkDisplayName", () => { 397 + describe("Global allow list", () => { 398 + it("should skip profiles from globally allowed DIDs", async () => { 399 + await checkDisplayName( 400 + "did:plc:globalallow", 401 + mockTime, 402 + mockDisplayName, 403 + mockDescription, 404 + ); 405 + 406 + expect(logger.warn).toHaveBeenCalledWith( 407 + { 408 + process: "CHECKDISPLAYNAME", 409 + did: "did:plc:globalallow", 410 + time: mockTime, 411 + displayName: mockDisplayName, 412 + description: mockDescription, 413 + }, 414 + "Global AllowListed DID", 415 + ); 416 + expect(createAccountLabel).not.toHaveBeenCalled(); 417 + }); 418 + 419 + it("should process non-globally-allowed DIDs", async () => { 420 + await checkDisplayName(mockDid, mockTime, "fake user", mockDescription); 421 + 422 + expect(logger.warn).not.toHaveBeenCalledWith( 423 + expect.objectContaining({ did: mockDid }), 424 + "Global AllowListed DID", 425 + ); 426 + }); 427 + }); 428 + 429 + describe("Pattern matching and labeling", () => { 430 + it("should label profiles with matching display names", async () => { 431 + await checkDisplayName( 432 + mockDid, 433 + mockTime, 434 + "fake account", 435 + mockDescription, 436 + ); 437 + 438 + expect(logger.info).toHaveBeenCalledWith( 439 + { 440 + process: "CHECKDISPLAYNAME", 441 + did: mockDid, 442 + time: mockTime, 443 + displayName: "fake account", 444 + description: mockDescription, 445 + label: "test-displayname", 446 + }, 447 + "Labeling account", 448 + ); 449 + expect(createAccountLabel).toHaveBeenCalledWith( 450 + mockDid, 451 + "test-displayname", 452 + expect.stringContaining("Test display name check"), 453 + ); 454 + }); 455 + 456 + it("should not label profiles without matching display names", async () => { 457 + await checkDisplayName(mockDid, mockTime, "Normal User", mockDescription); 458 + 459 + expect(createAccountLabel).not.toHaveBeenCalledWith( 460 + mockDid, 461 + "test-displayname", 462 + expect.any(String), 463 + ); 464 + }); 465 + 466 + it("should not check displayName when displayName field is false", async () => { 467 + await checkDisplayName( 468 + mockDid, 469 + mockTime, 470 + mockDisplayName, 471 + "spam description", 472 + ); 473 + 474 + // test-description has description: true, displayName: false 475 + // so it should not trigger on displayName content 476 + expect(createAccountLabel).not.toHaveBeenCalledWith( 477 + mockDid, 478 + "test-description", 479 + expect.any(String), 480 + ); 481 + }); 482 + 483 + it("should handle empty display name", async () => { 484 + await checkDisplayName(mockDid, mockTime, "", mockDescription); 485 + 486 + expect(createAccountLabel).not.toHaveBeenCalled(); 487 + }); 488 + }); 489 + 490 + describe("Language filtering", () => { 491 + it("should check language-specific patterns for matching languages", async () => { 492 + vi.mocked(getLanguage).mockResolvedValue("eng"); 493 + 494 + await checkDisplayName( 495 + mockDid, 496 + mockTime, 497 + "hello world", 498 + "description", 499 + ); 500 + 501 + // language-specific check has description: true, displayName: false 502 + // so it won't match on displayName 503 + expect(createAccountLabel).not.toHaveBeenCalledWith( 504 + mockDid, 505 + "language-specific", 506 + expect.any(String), 507 + ); 508 + }); 509 + 510 + it("should skip language-specific patterns for non-matching languages", async () => { 511 + vi.mocked(getLanguage).mockResolvedValue("spa"); 512 + 513 + await checkDisplayName(mockDid, mockTime, "hello", mockDescription); 514 + 515 + expect(createAccountLabel).not.toHaveBeenCalledWith( 516 + mockDid, 517 + "language-specific", 518 + expect.any(String), 519 + ); 520 + }); 521 + }); 522 + 523 + describe("Whitelist handling", () => { 524 + it("should skip patterns when whitelist matches", async () => { 525 + await checkDisplayName( 526 + mockDid, 527 + mockTime, 528 + "good bad user", 529 + mockDescription, 530 + ); 531 + 532 + // whitelisted-test has description: true, displayName: false 533 + // so it won't trigger on displayName 534 + expect(createAccountLabel).not.toHaveBeenCalledWith( 535 + mockDid, 536 + "whitelisted-test", 537 + expect.any(String), 538 + ); 539 + }); 540 + }); 541 + 542 + describe("Ignored DIDs", () => { 543 + it("should skip checks for ignored DIDs", async () => { 544 + await checkDisplayName( 545 + "did:plc:ignored123", 546 + mockTime, 547 + "test user", 548 + mockDescription, 549 + ); 550 + 551 + expect(logger.debug).toHaveBeenCalledWith( 552 + { 553 + process: "CHECKDISPLAYNAME", 554 + did: "did:plc:ignored123", 555 + time: mockTime, 556 + displayName: "test user", 557 + description: mockDescription, 558 + }, 559 + "Whitelisted DID", 560 + ); 561 + expect(createAccountLabel).not.toHaveBeenCalledWith( 562 + "did:plc:ignored123", 563 + "ignored-did", 564 + expect.any(String), 565 + ); 566 + }); 567 + 568 + it("should check non-ignored DIDs", async () => { 569 + await checkDisplayName(mockDid, mockTime, "test user", mockDescription); 570 + 571 + // ignored-did has description: true, displayName: false 572 + // so it won't trigger on displayName 573 + expect(createAccountLabel).not.toHaveBeenCalledWith( 574 + mockDid, 575 + "ignored-did", 576 + expect.any(String), 577 + ); 578 + }); 579 + }); 580 + 581 + describe("Moderation actions", () => { 582 + it("should execute all moderation actions when enabled", async () => { 583 + await checkDisplayName( 584 + mockDid, 585 + mockTime, 586 + mockDisplayName, 587 + "report this content", 588 + ); 589 + 590 + // all-actions has description: true, displayName: false 591 + // so it won't match on displayName 592 + expect(createAccountLabel).not.toHaveBeenCalledWith( 593 + mockDid, 594 + "all-actions", 595 + expect.any(String), 596 + ); 597 + }); 598 + }); 599 + 600 + describe("Both fields check", () => { 601 + it("should check displayName when both fields are enabled", async () => { 602 + await checkDisplayName( 603 + mockDid, 604 + mockTime, 605 + "suspicious user", 606 + mockDescription, 607 + ); 608 + 609 + expect(createAccountLabel).toHaveBeenCalledWith( 610 + mockDid, 611 + "both-fields", 612 + expect.any(String), 613 + ); 614 + }); 615 + 616 + it("should check description when both fields are enabled", async () => { 617 + await checkDescription( 618 + mockDid, 619 + mockTime, 620 + mockDisplayName, 621 + "suspicious content", 622 + ); 623 + 624 + expect(createAccountLabel).toHaveBeenCalledWith( 625 + mockDid, 626 + "both-fields", 627 + expect.any(String), 628 + ); 629 + }); 630 + }); 631 + }); 632 + });