A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules
at main 5.3 kB view raw
1import { describe, it, expect, vi, beforeEach } from "vitest"; 2import { getAllAccountLabels, negateAccountLabel } from "../accountModeration.js"; 3import { agent } from "../agent.js"; 4 5vi.mock("../agent.js", () => ({ 6 agent: { 7 did: "did:plc:test-moderator", 8 tools: { 9 ozone: { 10 moderation: { 11 getRepo: vi.fn(), 12 emitEvent: vi.fn(), 13 }, 14 }, 15 }, 16 }, 17 isLoggedIn: Promise.resolve(), 18})); 19 20vi.mock("../logger.js", () => ({ 21 logger: { 22 info: vi.fn(), 23 debug: vi.fn(), 24 error: vi.fn(), 25 warn: vi.fn(), 26 }, 27})); 28 29vi.mock("../limits.js", () => ({ 30 limit: vi.fn((fn) => fn()), 31})); 32 33vi.mock("../redis.js", () => ({ 34 deleteAccountLabelClaim: vi.fn().mockResolvedValue(undefined), 35})); 36 37vi.mock("../metrics.js", () => ({ 38 unlabelsRemovedCounter: { 39 inc: vi.fn(), 40 }, 41 labelsAppliedCounter: { 42 inc: vi.fn(), 43 }, 44 labelsCachedCounter: { 45 inc: vi.fn(), 46 }, 47})); 48 49const mockAgent = agent as any; 50 51describe("getAllAccountLabels", () => { 52 beforeEach(() => { 53 vi.clearAllMocks(); 54 }); 55 56 it("should return array of label strings from API response", async () => { 57 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 58 data: { 59 labels: [{ val: "blue-heart-emoji" }, { val: "hammer-sickle" }], 60 }, 61 }); 62 63 const labels = await getAllAccountLabels("did:plc:test123"); 64 65 expect(labels).toEqual(["blue-heart-emoji", "hammer-sickle"]); 66 expect(mockAgent.tools.ozone.moderation.getRepo).toHaveBeenCalledWith( 67 { did: "did:plc:test123" }, 68 expect.objectContaining({ 69 headers: expect.any(Object), 70 }), 71 ); 72 }); 73 74 it("should return empty array when account has no labels", async () => { 75 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 76 data: { 77 labels: undefined, 78 }, 79 }); 80 81 const labels = await getAllAccountLabels("did:plc:test123"); 82 83 expect(labels).toEqual([]); 84 }); 85 86 it("should return empty array when labels array is empty", async () => { 87 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 88 data: { 89 labels: [], 90 }, 91 }); 92 93 const labels = await getAllAccountLabels("did:plc:test123"); 94 95 expect(labels).toEqual([]); 96 }); 97 98 it("should return empty array on API error", async () => { 99 mockAgent.tools.ozone.moderation.getRepo.mockRejectedValueOnce( 100 new Error("API Error"), 101 ); 102 103 const labels = await getAllAccountLabels("did:plc:test123"); 104 105 expect(labels).toEqual([]); 106 }); 107}); 108 109describe("negateAccountLabel", () => { 110 beforeEach(() => { 111 vi.clearAllMocks(); 112 }); 113 114 it("should emit moderation event to remove label", async () => { 115 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 116 data: { 117 labels: [{ val: "blue-heart-emoji" }], 118 }, 119 }); 120 121 mockAgent.tools.ozone.moderation.emitEvent.mockResolvedValueOnce({}); 122 123 await negateAccountLabel( 124 "did:plc:test123", 125 "blue-heart-emoji", 126 "Test removal", 127 ); 128 129 expect(mockAgent.tools.ozone.moderation.emitEvent).toHaveBeenCalledWith( 130 expect.objectContaining({ 131 event: expect.objectContaining({ 132 $type: "tools.ozone.moderation.defs#modEventLabel", 133 createLabelVals: [], 134 negateLabelVals: ["blue-heart-emoji"], 135 comment: "Test removal", 136 }), 137 subject: expect.objectContaining({ 138 $type: "com.atproto.admin.defs#repoRef", 139 did: "did:plc:test123", 140 }), 141 }), 142 expect.any(Object), 143 ); 144 }); 145 146 it("should not emit event if label does not exist on account", async () => { 147 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 148 data: { 149 labels: [{ val: "other-label" }], 150 }, 151 }); 152 153 await negateAccountLabel( 154 "did:plc:test123", 155 "blue-heart-emoji", 156 "Test removal", 157 ); 158 159 expect(mockAgent.tools.ozone.moderation.emitEvent).not.toHaveBeenCalled(); 160 }); 161 162 it("should not emit event if account has no labels", async () => { 163 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 164 data: { 165 labels: [], 166 }, 167 }); 168 169 await negateAccountLabel( 170 "did:plc:test123", 171 "blue-heart-emoji", 172 "Test removal", 173 ); 174 175 expect(mockAgent.tools.ozone.moderation.emitEvent).not.toHaveBeenCalled(); 176 }); 177 178 it("should delete Redis cache key on successful removal", async () => { 179 const { deleteAccountLabelClaim } = await import("../redis.js"); 180 181 mockAgent.tools.ozone.moderation.getRepo.mockResolvedValueOnce({ 182 data: { 183 labels: [{ val: "blue-heart-emoji" }], 184 }, 185 }); 186 187 mockAgent.tools.ozone.moderation.emitEvent.mockResolvedValueOnce({}); 188 189 await negateAccountLabel( 190 "did:plc:test123", 191 "blue-heart-emoji", 192 "Test removal", 193 ); 194 195 expect(deleteAccountLabelClaim).toHaveBeenCalledWith( 196 "did:plc:test123", 197 "blue-heart-emoji", 198 ); 199 }); 200 201 it("should log error if API call fails", async () => { 202 const { logger } = await import("../logger.js"); 203 204 mockAgent.tools.ozone.moderation.getRepo.mockRejectedValueOnce( 205 new Error("API Error"), 206 ); 207 208 await negateAccountLabel( 209 "did:plc:test123", 210 "blue-heart-emoji", 211 "Test removal", 212 ); 213 214 expect(logger.error).toHaveBeenCalled(); 215 }); 216});