Skip to content

Commit 8249fb6

Browse files
authored
Merge pull request #6479 from mozilla/MNTOR-4502
fix(syncBreaches): only upload logos that do not exist in s3
2 parents c6a270c + 837da1a commit 8249fb6

File tree

6 files changed

+342
-92
lines changed

6 files changed

+342
-92
lines changed

src/db/tables/breaches.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,21 @@ async function updateBreachFaviconUrl(name: string, faviconUrl: string | null) {
8181
favicon_url: faviconUrl,
8282
});
8383
}
84+
85+
async function getBreachFaviconUrl(name: string) {
86+
const res = await knex("breaches")
87+
.where("name", name)
88+
.select("favicon_url")
89+
.first();
90+
return res?.favicon_url;
91+
}
8492
/* c8 ignore stop */
8593

8694
export {
8795
getAllBreaches,
8896
getAllBreachesCount,
8997
upsertBreaches,
9098
updateBreachFaviconUrl,
99+
getBreachFaviconUrl,
91100
knex,
92101
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import * as Sentry from "@sentry/node";
6+
import { config } from "../../../config";
7+
import { run } from "./syncBreaches";
8+
9+
Sentry.init({
10+
dsn: config.sentryDsn,
11+
tracesSampleRate: 1.0,
12+
});
13+
14+
const SENTRY_SLUG = "cron-sync-breaches";
15+
16+
const checkInId = Sentry.captureCheckIn({
17+
monitorSlug: SENTRY_SLUG,
18+
status: "in_progress",
19+
});
20+
21+
try {
22+
await run();
23+
Sentry.captureCheckIn({
24+
checkInId,
25+
monitorSlug: SENTRY_SLUG,
26+
status: "ok",
27+
});
28+
} catch (error) {
29+
Sentry.captureCheckIn({
30+
checkInId,
31+
monitorSlug: SENTRY_SLUG,
32+
status: "error",
33+
});
34+
throw error;
35+
}
36+
setTimeout(process.exit, 1000);
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
/**
6+
* @vitest-environment node
7+
*/
8+
9+
import {
10+
vi,
11+
describe,
12+
it,
13+
expect,
14+
beforeEach,
15+
afterEach,
16+
afterAll,
17+
type MockInstance,
18+
} from "vitest";
19+
import hibpBreachMock from "../../../test/seeds/hibpBreachResponse.json";
20+
import { getBreachIcons } from "./syncBreaches";
21+
import { HibpGetBreachesResponse } from "../../../utils/hibp";
22+
import { uploadToS3, checkS3ObjectExists } from "../../../utils/s3";
23+
import {
24+
getBreachFaviconUrl,
25+
updateBreachFaviconUrl,
26+
} from "../../../db/tables/breaches";
27+
28+
vi.mock("../../../utils/s3", () => ({
29+
uploadToS3: vi.fn().mockResolvedValue(undefined),
30+
checkS3ObjectExists: vi.fn().mockResolvedValue(false),
31+
}));
32+
33+
vi.mock("../../../db/tables/breaches", () => ({
34+
updateBreachFaviconUrl: vi.fn().mockResolvedValue(undefined),
35+
getBreachFaviconUrl: vi.fn(),
36+
}));
37+
38+
vi.mock("../../../app/functions/server/logging", async () => {
39+
const { mockLogger } = await import("../../../test/helpers/mockLogger");
40+
return {
41+
logger: mockLogger(),
42+
};
43+
});
44+
45+
const mockGetBreachFaviconUrls = (
46+
favicons: Array<string | null | undefined>,
47+
) => {
48+
vi.mocked(getBreachFaviconUrl).mockReset();
49+
favicons.forEach((favicon) =>
50+
vi.mocked(getBreachFaviconUrl).mockResolvedValueOnce(favicon),
51+
);
52+
};
53+
54+
const buildBreachFavicon = (breachDomain: string) =>
55+
`https://s3.amazonaws.com/${process.env.S3_BUCKET}/${breachDomain.toLowerCase()}.ico`;
56+
57+
describe("syncBreaches", () => {
58+
describe("getBreachIcons", () => {
59+
const breaches = hibpBreachMock.slice(0, 2) as HibpGetBreachesResponse;
60+
let fetchSpy: MockInstance;
61+
62+
beforeEach(() => {
63+
fetchSpy = vi.spyOn(global, "fetch");
64+
// Set up default mocks which return a synced
65+
// favicon record (only checked if s3 object exists)
66+
mockGetBreachFaviconUrls(
67+
breaches.map((breach) => buildBreachFavicon(breach.Domain)),
68+
);
69+
});
70+
71+
afterEach(() => {
72+
vi.resetAllMocks();
73+
});
74+
afterAll(() => vi.restoreAllMocks());
75+
76+
it("only fetches and uploads icons not already in s3", async () => {
77+
fetchSpy.mockResolvedValue({
78+
status: 200,
79+
arrayBuffer: async () => Buffer.from("abc"),
80+
} as unknown as Response);
81+
vi.mocked(checkS3ObjectExists)
82+
.mockResolvedValueOnce(true)
83+
.mockResolvedValue(false);
84+
await getBreachIcons(breaches);
85+
expect(uploadToS3).toHaveBeenCalledTimes(breaches.length - 1);
86+
});
87+
it("skips breaches without domains", async () => {
88+
const noDomains = breaches.map((breach) => ({ ...breach, Domain: "" }));
89+
await getBreachIcons(noDomains);
90+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(noDomains.length);
91+
expect(uploadToS3).toHaveBeenCalledTimes(0);
92+
});
93+
it("syncs db value if logo is in s3 but not the same as in the db", async () => {
94+
const favicons = [
95+
undefined,
96+
"not-a-match",
97+
buildBreachFavicon(breaches[0].Domain),
98+
];
99+
vi.mocked(checkS3ObjectExists).mockResolvedValue(true);
100+
mockGetBreachFaviconUrls(favicons);
101+
await getBreachIcons([breaches[0], breaches[1], breaches[0]]);
102+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(2);
103+
expect(updateBreachFaviconUrl).toHaveBeenNthCalledWith(
104+
1,
105+
breaches[0].Name,
106+
buildBreachFavicon(breaches[0].Domain),
107+
);
108+
expect(updateBreachFaviconUrl).toHaveBeenNthCalledWith(
109+
2,
110+
breaches[1].Name,
111+
buildBreachFavicon(breaches[1].Domain),
112+
);
113+
});
114+
it("skips uploading if logo fetch status code is not 200", async () => {
115+
fetchSpy
116+
.mockResolvedValueOnce({
117+
status: 500,
118+
} as unknown as Response)
119+
.mockResolvedValue({
120+
status: 200,
121+
arrayBuffer: async () => Buffer.from("abc"),
122+
} as unknown as Response);
123+
await getBreachIcons(breaches);
124+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(breaches.length);
125+
expect(uploadToS3).toHaveBeenCalledTimes(breaches.length - 1);
126+
});
127+
it("continues processing if error is thrown in fetch", async () => {
128+
fetchSpy.mockRejectedValueOnce(new Error("nope")).mockResolvedValue({
129+
status: 200,
130+
arrayBuffer: async () => Buffer.from("abc"),
131+
} as unknown as Response);
132+
await getBreachIcons(breaches);
133+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(breaches.length - 1);
134+
expect(uploadToS3).toHaveBeenCalledTimes(breaches.length - 1);
135+
});
136+
it("continues processing if error is thrown in checkS3ObjectExists", async () => {
137+
fetchSpy.mockResolvedValue({
138+
status: 200,
139+
arrayBuffer: async () => Buffer.from("abc"),
140+
} as unknown as Response);
141+
vi.mocked(checkS3ObjectExists)
142+
.mockRejectedValueOnce(new Error("nope"))
143+
.mockResolvedValue(false);
144+
await getBreachIcons(breaches);
145+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(breaches.length - 1);
146+
expect(uploadToS3).toHaveBeenCalledTimes(breaches.length - 1);
147+
});
148+
it("continues processing if error is thrown in updateBreachFaviconUrl", async () => {
149+
fetchSpy.mockResolvedValue({
150+
status: 200,
151+
arrayBuffer: async () => Buffer.from("abc"),
152+
} as unknown as Response);
153+
vi.mocked(updateBreachFaviconUrl)
154+
.mockRejectedValueOnce(new Error("nope"))
155+
.mockResolvedValue(undefined);
156+
await getBreachIcons(breaches);
157+
// uploadToS3 is called before updateBreachFaviconurl
158+
expect(uploadToS3).toHaveBeenCalledTimes(breaches.length);
159+
// still called the same number of times although one throws
160+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(breaches.length);
161+
});
162+
it("continues processing if error is thrown in uploadToS3", async () => {
163+
fetchSpy.mockResolvedValue({
164+
status: 200,
165+
arrayBuffer: async () => Buffer.from("abc"),
166+
} as unknown as Response);
167+
vi.mocked(uploadToS3)
168+
.mockRejectedValueOnce(new Error("nope"))
169+
.mockResolvedValue(undefined);
170+
await getBreachIcons(breaches);
171+
expect(uploadToS3).toHaveBeenCalledTimes(breaches.length);
172+
expect(updateBreachFaviconUrl).toHaveBeenCalledTimes(breaches.length - 1);
173+
});
174+
});
175+
});

0 commit comments

Comments
 (0)