A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
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});