mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-05-17 10:11:21 +00:00
## Release Notes Thanks to: @cgillinger, @khassel, @KristjanESPERANTO, @sonnyb9 > ⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 (no change to previous release) [Compare to previous Release v2.35.0](https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.35.0...v2.36.0) This release falls outside the quarterly schedule. We opted for an early release due to: - Security fix for the internal cors proxy - API change of the weather provider smi - Several bug fixes ### Breaking Changes The cors proxy is now disabled by default. If required, it must be explicitly enabled in the `config.js` file. See the [documentation](https://docs.magicmirror.builders/configuration/cors.html). ### ⚠️ Security You can find several publicly accessible MagicMirror² instances. This should never be done. Doing so makes your entire configuration, including secrets and API keys, publicly visible. Furthermore, it allows attackers to target the host; this is only prevented beginning with this release. Public MagicMirror² instances should always run behind a reverse proxy with authentication. ### [core] - Prepare Release 2.36.0 (#4126) - Allow HTTPFetcher to pass through 304 responses (#4120) - fix(http-fetcher): fall back to reloadInterval after retries exhausted (#4113) - config endpoint must handle functions in module configs (#4106) - fix replaceSecretPlaceholder (#4104) - restrict replaceSecretPlaceholder to cors with allowWhitelist (#4102) - fix: prevent crash when config is undefined in socket handler (#4096) - fix cors function for alpine linux (#4091) - fix(cors): prevent SSRF via DNS rebinding (#4090) - add option to disable or restrict cors endpoint (#4087) - fix: prevent SSRF via /cors endpoint by blocking private/reserved IPs (#4084) - chore: add permissions section to enforce pull-request rules workflow (#4079) - update version for develop ### [dependencies] - update dependencies (#4124) - chore: update dependencies (#4088) - refactor: enable ESLint rule "no-unused-vars" and handle related issues (#4080) ### [modules/newsfeed] - fix(newsfeed): prevent duplicate parse error callback when using pipeline (#4083) ### [modules/updatenotification] - fix(updatenotification): harden git command execution + simplify checkUpdates (#4115) - fix(tests): correct import path for git_helper module in updatenotification tests (#4078) ### [modules/weather] - fix(weather): use nearest openmeteo hourly data (#4123) - fix(weather): avoid loading state after reconnect (#4121) - weather: fix UV index display and add WeatherFlow precipitation (#4108) - fix(weather): restore OpenWeatherMap v2.5 support (#4101) - fix(weather): use stable instanceId to prevent duplicate fetchers (#4092) - SMHI: migrate to SNOW1gv1 API (replace deprecated PMP3gv2) (#4082) ### [testing] - ci(actions): set explicit token permissions (#4114) - fix(http_fetcher): use undici.fetch when dispatcher is present (#4097) - ci(codeql): also scan develop branch on push and PR (#4086) - refactor: replace implicit global config with explicit global.config (#4085) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <gitkraken@veeck.de> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com> Co-authored-by: Nathan <n8nyoung@gmail.com> Co-authored-by: mixasgr <mixasgr@users.noreply.github.com> Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr> Co-authored-by: Konstantinos <geraki@gmail.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com> Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: Koen Konst <koenspero@gmail.com> Co-authored-by: Koen Konst <c.h.konst@avisi.nl> Co-authored-by: dathbe <github@beffa.us> Co-authored-by: Marcel <m-idler@users.noreply.github.com> Co-authored-by: Kevin G. <crazylegstoo@gmail.com> Co-authored-by: Jboucly <33218155+jboucly@users.noreply.github.com> Co-authored-by: Jboucly <contact@jboucly.fr> Co-authored-by: Jarno <54169345+jarnoml@users.noreply.github.com> Co-authored-by: Jordan Welch <JordanHWelch@gmail.com> Co-authored-by: Blackspirits <blackspirits@gmail.com> Co-authored-by: Samed Ozdemir <samed@xsor.io> Co-authored-by: in-voker <58696565+in-voker@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: cgillinger <christian.gillinger@gmail.com> Co-authored-by: Sonny B <43247590+sonnyb9@users.noreply.github.com> Co-authored-by: sonnyb9 <sonnyb9@users.noreply.github.com>
577 lines
15 KiB
JavaScript
577 lines
15 KiB
JavaScript
const { http, HttpResponse } = require("msw");
|
|
const { setupServer } = require("msw/node");
|
|
const HTTPFetcher = require("#http_fetcher");
|
|
|
|
const TEST_URL = "http://test.example.com/data";
|
|
let server;
|
|
let fetcher;
|
|
|
|
beforeAll(() => {
|
|
server = setupServer();
|
|
server.listen({ onUnhandledRequest: "error" });
|
|
});
|
|
|
|
afterAll(() => {
|
|
server.close();
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.resetHandlers();
|
|
if (fetcher) {
|
|
fetcher.clearTimer();
|
|
fetcher = null;
|
|
}
|
|
});
|
|
|
|
describe("HTTPFetcher", () => {
|
|
|
|
describe("Basic fetch operations", () => {
|
|
it("should emit response event on successful fetch", async () => {
|
|
const responseData = "test data";
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return HttpResponse.text(responseData);
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", (response) => {
|
|
resolve(response);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const response = await responsePromise;
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.status).toBe(200);
|
|
const text = await response.text();
|
|
expect(text).toBe(responseData);
|
|
});
|
|
|
|
it("should treat 304 responses as successful and reset error counters", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 304 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
fetcher.serverErrorCount = 2;
|
|
fetcher.networkErrorCount = 3;
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", (response) => {
|
|
resolve(response);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const response = await responsePromise;
|
|
|
|
expect(response.status).toBe(304);
|
|
expect(fetcher.serverErrorCount).toBe(0);
|
|
expect(fetcher.networkErrorCount).toBe(0);
|
|
});
|
|
|
|
it("should emit error event on network failure", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return HttpResponse.error();
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo).toHaveProperty("errorType", "NETWORK_ERROR");
|
|
expect(errorInfo).toHaveProperty("translationKey", "MODULE_ERROR_NO_CONNECTION");
|
|
expect(errorInfo).toHaveProperty("url", TEST_URL);
|
|
});
|
|
|
|
it("should emit error event on timeout", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, async () => {
|
|
// Simulate a slow server that never responds
|
|
await new Promise((resolve) => setTimeout(resolve, 60000));
|
|
return HttpResponse.text("too late");
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000, timeout: 100 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.errorType).toBe("NETWORK_ERROR");
|
|
expect(errorInfo.message).toContain("timeout");
|
|
expect(errorInfo.message).toContain("100ms");
|
|
});
|
|
});
|
|
|
|
describe("HTTPFetcher - HTTP status code handling", () => {
|
|
describe("401/403 errors (Auth failures)", () => {
|
|
it("should emit error with AUTH_FAILURE for 401", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 401 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.status).toBe(401);
|
|
expect(errorInfo.errorType).toBe("AUTH_FAILURE");
|
|
expect(errorInfo.translationKey).toBe("MODULE_ERROR_UNAUTHORIZED");
|
|
});
|
|
|
|
it("should emit error with AUTH_FAILURE for 403", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 403 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.status).toBe(403);
|
|
expect(errorInfo.errorType).toBe("AUTH_FAILURE");
|
|
});
|
|
});
|
|
|
|
describe("429 errors (Rate limiting)", () => {
|
|
it("should emit error with RATE_LIMITED for 429", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, {
|
|
status: 429,
|
|
headers: { "Retry-After": "120" }
|
|
});
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.status).toBe(429);
|
|
expect(errorInfo.errorType).toBe("RATE_LIMITED");
|
|
expect(errorInfo.retryAfter).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should parse Retry-After header in seconds", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, {
|
|
status: 429,
|
|
headers: { "Retry-After": "300" }
|
|
});
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
// 300 seconds = 300000 ms
|
|
expect(errorInfo.retryAfter).toBe(300000);
|
|
});
|
|
});
|
|
|
|
describe("5xx errors (Server errors)", () => {
|
|
it("should emit error with SERVER_ERROR for 500", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 500 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.status).toBe(500);
|
|
expect(errorInfo.errorType).toBe("SERVER_ERROR");
|
|
});
|
|
|
|
it("should emit error with SERVER_ERROR for 503", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 503 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.status).toBe(503);
|
|
expect(errorInfo.errorType).toBe("SERVER_ERROR");
|
|
});
|
|
});
|
|
|
|
describe("4xx errors (Client errors)", () => {
|
|
it("should emit error with CLIENT_ERROR for 404", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 404 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", (errorInfo) => {
|
|
resolve(errorInfo);
|
|
});
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.status).toBe(404);
|
|
expect(errorInfo.errorType).toBe("CLIENT_ERROR");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("HTTPFetcher - Authentication", () => {
|
|
it("should include Basic auth header when configured", async () => {
|
|
let receivedHeaders = null;
|
|
|
|
server.use(
|
|
http.get(TEST_URL, ({ request }) => {
|
|
receivedHeaders = Object.fromEntries(request.headers);
|
|
return HttpResponse.text("ok");
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, {
|
|
reloadInterval: 60000,
|
|
auth: {
|
|
method: "basic",
|
|
user: "testuser",
|
|
pass: "testpass"
|
|
}
|
|
});
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", resolve);
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
await responsePromise;
|
|
|
|
const expectedAuth = `Basic ${Buffer.from("testuser:testpass").toString("base64")}`;
|
|
expect(receivedHeaders.authorization).toBe(expectedAuth);
|
|
});
|
|
|
|
it("should include Bearer auth header when configured", async () => {
|
|
let receivedHeaders = null;
|
|
|
|
server.use(
|
|
http.get(TEST_URL, ({ request }) => {
|
|
receivedHeaders = Object.fromEntries(request.headers);
|
|
return HttpResponse.text("ok");
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, {
|
|
reloadInterval: 60000,
|
|
auth: {
|
|
method: "bearer",
|
|
pass: "my-token-123"
|
|
}
|
|
});
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", resolve);
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
await responsePromise;
|
|
|
|
expect(receivedHeaders.authorization).toBe("Bearer my-token-123");
|
|
});
|
|
});
|
|
|
|
describe("Custom headers", () => {
|
|
it("should include custom headers in request", async () => {
|
|
let receivedHeaders = null;
|
|
|
|
server.use(
|
|
http.get(TEST_URL, ({ request }) => {
|
|
receivedHeaders = Object.fromEntries(request.headers);
|
|
return HttpResponse.text("ok");
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, {
|
|
reloadInterval: 60000,
|
|
headers: {
|
|
"X-Custom-Header": "custom-value",
|
|
Accept: "application/json"
|
|
}
|
|
});
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", resolve);
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
await responsePromise;
|
|
|
|
expect(receivedHeaders["x-custom-header"]).toBe("custom-value");
|
|
expect(receivedHeaders.accept).toBe("application/json");
|
|
});
|
|
});
|
|
|
|
describe("Timer management", () => {
|
|
it("should not set timer in test mode", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return HttpResponse.text("ok");
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 100 });
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", resolve);
|
|
});
|
|
|
|
fetcher.startPeriodicFetch();
|
|
await responsePromise;
|
|
|
|
// Timer should NOT be set in test mode (mmTestMode=true)
|
|
expect(fetcher.reloadTimer).toBeNull();
|
|
});
|
|
|
|
it("should clear timer when clearTimer is called", () => {
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 100 });
|
|
|
|
// Manually set a timer to test clearing
|
|
fetcher.reloadTimer = setTimeout(() => {}, 10000);
|
|
expect(fetcher.reloadTimer).not.toBeNull();
|
|
|
|
fetcher.clearTimer();
|
|
|
|
expect(fetcher.reloadTimer).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("fetch() method", () => {
|
|
it("should emit response event when called", async () => {
|
|
const responseData = "direct fetch data";
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return HttpResponse.text(responseData);
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const responsePromise = new Promise((resolve) => {
|
|
fetcher.on("response", resolve);
|
|
});
|
|
|
|
await fetcher.fetch();
|
|
const response = await responsePromise;
|
|
|
|
expect(response.ok).toBe(true);
|
|
const text = await response.text();
|
|
expect(text).toBe(responseData);
|
|
});
|
|
|
|
it("should emit error event on network error", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return HttpResponse.error();
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
|
|
|
const errorPromise = new Promise((resolve) => {
|
|
fetcher.on("error", resolve);
|
|
});
|
|
|
|
await fetcher.fetch();
|
|
const errorInfo = await errorPromise;
|
|
|
|
expect(errorInfo.errorType).toBe("NETWORK_ERROR");
|
|
});
|
|
});
|
|
|
|
describe("selfSignedCert dispatcher", () => {
|
|
const { Agent } = require("undici");
|
|
|
|
it("should set rejectUnauthorized=false when selfSignedCert is true", () => {
|
|
fetcher = new HTTPFetcher(TEST_URL, {
|
|
reloadInterval: 60000,
|
|
selfSignedCert: true
|
|
});
|
|
|
|
const options = fetcher.getRequestOptions();
|
|
|
|
expect(options.dispatcher).toBeInstanceOf(Agent);
|
|
const agentOptionsSymbol = Object.getOwnPropertySymbols(options.dispatcher).find((s) => s.description === "options");
|
|
const dispatcherOptions = options.dispatcher[agentOptionsSymbol];
|
|
expect(dispatcherOptions.connect.rejectUnauthorized).toBe(false);
|
|
});
|
|
|
|
it("should not set a dispatcher when selfSignedCert is false", () => {
|
|
fetcher = new HTTPFetcher(TEST_URL, {
|
|
reloadInterval: 60000,
|
|
selfSignedCert: false
|
|
});
|
|
|
|
const options = fetcher.getRequestOptions();
|
|
|
|
expect(options.dispatcher).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("Retry exhaustion fallback", () => {
|
|
it("should fall back to reloadInterval after network retries exhausted", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return HttpResponse.error();
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 300000, maxRetries: 3 });
|
|
|
|
const errors = [];
|
|
fetcher.on("error", (errorInfo) => errors.push(errorInfo));
|
|
|
|
// Trigger maxRetries + 1 fetches to reach exhaustion
|
|
for (let i = 0; i < 4; i++) {
|
|
await fetcher.fetch();
|
|
}
|
|
|
|
// First retries should use backoff (< reloadInterval)
|
|
expect(errors[0].retryAfter).toBe(15000);
|
|
expect(errors[1].retryAfter).toBe(30000);
|
|
// Third retry hits maxRetries, should fall back to reloadInterval
|
|
expect(errors[2].retryAfter).toBe(300000);
|
|
// Subsequent errors stay at reloadInterval
|
|
expect(errors[3].retryAfter).toBe(300000);
|
|
});
|
|
|
|
it("should fall back to reloadInterval after server error retries exhausted", async () => {
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
return new HttpResponse(null, { status: 503 });
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 300000, maxRetries: 3 });
|
|
|
|
const errors = [];
|
|
fetcher.on("error", (errorInfo) => errors.push(errorInfo));
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
await fetcher.fetch();
|
|
}
|
|
|
|
// First retries should use backoff (< reloadInterval)
|
|
expect(errors[0].retryAfter).toBe(15000);
|
|
expect(errors[1].retryAfter).toBe(30000);
|
|
// Third retry hits maxRetries, should fall back to reloadInterval
|
|
expect(errors[2].retryAfter).toBe(300000);
|
|
// Subsequent errors stay at reloadInterval
|
|
expect(errors[3].retryAfter).toBe(300000);
|
|
});
|
|
|
|
it("should reset network error count on success", async () => {
|
|
let requestCount = 0;
|
|
server.use(
|
|
http.get(TEST_URL, () => {
|
|
requestCount++;
|
|
if (requestCount <= 2) return HttpResponse.error();
|
|
return HttpResponse.text("ok");
|
|
})
|
|
);
|
|
|
|
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 300000, maxRetries: 3 });
|
|
|
|
const errors = [];
|
|
fetcher.on("error", (errorInfo) => errors.push(errorInfo));
|
|
|
|
// Two failures with backoff
|
|
await fetcher.fetch();
|
|
await fetcher.fetch();
|
|
expect(errors).toHaveLength(2);
|
|
expect(errors[0].retryAfter).toBe(15000);
|
|
expect(errors[1].retryAfter).toBe(30000);
|
|
|
|
// Success resets counter
|
|
await fetcher.fetch();
|
|
expect(fetcher.networkErrorCount).toBe(0);
|
|
});
|
|
});
|