A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
at main 6.3 kB view raw
1import { afterEach, describe, expect, it, vi } from "vitest"; 2import { 3 createAccountComment, 4 createAccountLabel, 5 createAccountReport, 6} from "../accountModeration.js"; 7import { logger } from "../logger.js"; 8import { 9 starterPackLabelsThresholdAppliedCounter, 10 starterPackThresholdChecksCounter, 11 starterPackThresholdMetCounter, 12} from "../metrics.js"; 13import { 14 getStarterPackCountInWindow, 15 trackStarterPackForAccount, 16} from "../redis.js"; 17import { 18 checkStarterPackThreshold, 19 loadStarterPackThresholdConfigs, 20} from "../starterPackThreshold.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/starterPackThreshold.js", () => ({ 32 STARTER_PACK_THRESHOLD_CONFIGS: [ 33 { 34 threshold: 5, 35 window: 24, 36 windowUnit: "hours", 37 accountLabel: "starter-pack-spam", 38 accountComment: "Too many starter packs", 39 toLabel: true, 40 reportAcct: true, 41 commentAcct: false, 42 allowlist: ["did:plc:allowed123"], 43 }, 44 { 45 threshold: 10, 46 window: 7, 47 windowUnit: "days", 48 accountLabel: "starter-pack-abuse", 49 accountComment: "Excessive starter pack creation", 50 toLabel: true, 51 reportAcct: false, 52 commentAcct: true, 53 allowlist: [], 54 }, 55 ], 56})); 57 58vi.mock("../redis.js", () => ({ 59 trackStarterPackForAccount: vi.fn(), 60 getStarterPackCountInWindow: vi.fn(), 61})); 62 63vi.mock("../accountModeration.js", () => ({ 64 createAccountLabel: vi.fn(), 65 createAccountReport: vi.fn(), 66 createAccountComment: vi.fn(), 67})); 68 69vi.mock("../metrics.js", () => ({ 70 starterPackLabelsThresholdAppliedCounter: { 71 inc: vi.fn(), 72 }, 73 starterPackThresholdChecksCounter: { 74 inc: vi.fn(), 75 }, 76 starterPackThresholdMetCounter: { 77 inc: vi.fn(), 78 }, 79})); 80 81describe("Starter Pack Threshold Logic", () => { 82 afterEach(() => { 83 vi.clearAllMocks(); 84 }); 85 86 describe("loadStarterPackThresholdConfigs", () => { 87 it("should load and cache configs successfully", () => { 88 const configs = loadStarterPackThresholdConfigs(); 89 expect(configs).toHaveLength(2); 90 expect(configs[0].threshold).toBe(5); 91 expect(configs[1].threshold).toBe(10); 92 }); 93 }); 94 95 describe("checkStarterPackThreshold", () => { 96 const testDid = "did:plc:test123"; 97 const testUri = "at://did:plc:test123/app.bsky.graph.starterpack/abc"; 98 const testTimestamp = 1640000000000000; 99 100 it("should skip threshold check for allowlisted accounts", async () => { 101 vi.mocked(trackStarterPackForAccount).mockResolvedValue(); 102 vi.mocked(getStarterPackCountInWindow).mockResolvedValue(0); 103 104 await checkStarterPackThreshold( 105 "did:plc:allowed123", 106 testUri, 107 testTimestamp, 108 ); 109 110 expect(starterPackThresholdChecksCounter.inc).toHaveBeenCalled(); 111 // Should skip first config (allowlist), but process second config 112 expect(trackStarterPackForAccount).toHaveBeenCalledTimes(1); 113 expect(logger.debug).toHaveBeenCalledWith( 114 expect.objectContaining({ did: "did:plc:allowed123" }), 115 "Account is in allowlist, skipping threshold check", 116 ); 117 }); 118 119 it("should track and check threshold for non-allowlisted accounts", async () => { 120 vi.mocked(trackStarterPackForAccount).mockResolvedValue(); 121 vi.mocked(getStarterPackCountInWindow).mockResolvedValue(3); 122 123 await checkStarterPackThreshold(testDid, testUri, testTimestamp); 124 125 expect(starterPackThresholdChecksCounter.inc).toHaveBeenCalled(); 126 expect(trackStarterPackForAccount).toHaveBeenCalledWith( 127 testDid, 128 testUri, 129 testTimestamp, 130 24, 131 "hours", 132 ); 133 expect(getStarterPackCountInWindow).toHaveBeenCalledWith( 134 testDid, 135 24, 136 "hours", 137 testTimestamp, 138 ); 139 }); 140 141 it("should apply account label when threshold is met", async () => { 142 vi.mocked(trackStarterPackForAccount).mockResolvedValue(); 143 vi.mocked(getStarterPackCountInWindow).mockResolvedValue(5); 144 vi.mocked(createAccountLabel).mockResolvedValue(); 145 vi.mocked(createAccountReport).mockResolvedValue(); 146 147 await checkStarterPackThreshold(testDid, testUri, testTimestamp); 148 149 expect(starterPackThresholdMetCounter.inc).toHaveBeenCalledWith({ 150 account_label: "starter-pack-spam", 151 }); 152 expect(createAccountLabel).toHaveBeenCalledWith( 153 testDid, 154 "starter-pack-spam", 155 expect.stringContaining("Too many starter packs"), 156 ); 157 expect(createAccountReport).toHaveBeenCalled(); 158 expect(starterPackLabelsThresholdAppliedCounter.inc).toHaveBeenCalledWith({ 159 account_label: "starter-pack-spam", 160 action: "label", 161 }); 162 }); 163 164 it("should not apply label when threshold not met", async () => { 165 vi.mocked(trackStarterPackForAccount).mockResolvedValue(); 166 vi.mocked(getStarterPackCountInWindow).mockResolvedValue(3); 167 168 await checkStarterPackThreshold(testDid, testUri, testTimestamp); 169 170 expect(starterPackThresholdMetCounter.inc).not.toHaveBeenCalled(); 171 expect(createAccountLabel).not.toHaveBeenCalled(); 172 }); 173 174 it("should handle Redis errors", async () => { 175 const redisError = new Error("Redis connection failed"); 176 vi.mocked(trackStarterPackForAccount).mockRejectedValue(redisError); 177 178 await expect( 179 checkStarterPackThreshold(testDid, testUri, testTimestamp), 180 ).rejects.toThrow("Redis connection failed"); 181 182 expect(logger.error).toHaveBeenCalled(); 183 }); 184 185 it("should check all configs for each starter pack", async () => { 186 vi.mocked(trackStarterPackForAccount).mockResolvedValue(); 187 vi.mocked(getStarterPackCountInWindow) 188 .mockResolvedValueOnce(5) 189 .mockResolvedValueOnce(10); 190 vi.mocked(createAccountLabel).mockResolvedValue(); 191 vi.mocked(createAccountReport).mockResolvedValue(); 192 vi.mocked(createAccountComment).mockResolvedValue(); 193 194 await checkStarterPackThreshold(testDid, testUri, testTimestamp); 195 196 expect(trackStarterPackForAccount).toHaveBeenCalledTimes(2); 197 expect(getStarterPackCountInWindow).toHaveBeenCalledTimes(2); 198 expect(createAccountLabel).toHaveBeenCalledTimes(2); 199 }); 200 }); 201});