From 96f45e7094ea15ce990382c1de4db8eb6283090d Mon Sep 17 00:00:00 2001 From: Karelian Pie Date: Tue, 8 Mar 2022 18:55:59 +1100 Subject: [PATCH 1/5] test: Add cache unit tests --- src/cache.spec.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++ src/cache.ts | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/cache.spec.ts diff --git a/src/cache.spec.ts b/src/cache.spec.ts new file mode 100644 index 00000000..827370e1 --- /dev/null +++ b/src/cache.spec.ts @@ -0,0 +1,57 @@ +import { CachedFetcher } from "./cache"; +import { Context } from "./context"; + +const contextCacheSpy = jest.spyOn(Context.prototype, "cache", "get"); +const currentValueSpy = jest.spyOn(CachedFetcher.prototype, "currentValue", "get"); + +describe("CachedFetcher", () => { + let cachedFetcher: CachedFetcher; + + beforeEach(() => { + cachedFetcher = new CachedFetcher("path", new Context({}), 1); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("fetch", () => { + it("should return undefined when `useCache` is set to `false`", async () => { + contextCacheSpy.mockReturnValue({ + useCache: false, + url: "url" + }); + + const actualFetch = await cachedFetcher.fetch(); + + expect(actualFetch).toEqual(undefined); + }); + + it("should return undefined when `url` is not defined", async () => { + contextCacheSpy.mockReturnValue({ + useCache: true + }); + + const actualFetch = await cachedFetcher.fetch(); + + expect(actualFetch).toEqual(undefined); + }); + + describe("when useCache is `true` and there is a url", () => { + beforeAll(() => { + contextCacheSpy.mockReturnValue({ + useCache: true, + url: "url" + }); + }); + + it("should return cached when there is a current value", async () => { + currentValueSpy.mockReturnValue({ cachedValue: true }); + + const actualFetch = await cachedFetcher.fetch(); + + expect(actualFetch).toEqual({ cachedValue: true }); + }); + }); + }); +}); diff --git a/src/cache.ts b/src/cache.ts index fc48fd4d..bf735a02 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -64,7 +64,7 @@ export class CachedFetcher { ]); } - private get currentValue(): T | undefined { + get currentValue(): T | undefined { if (!this.expiryDate) { return undefined; } From 41e97e6bbd1ddceaf4c61ff9474bbe069d0531b5 Mon Sep 17 00:00:00 2001 From: Karelian Pie Date: Wed, 9 Mar 2022 15:13:43 +1100 Subject: [PATCH 2/5] test: Add further tests to cache.ts --- package.json | 2 +- src/cache.spec.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++++- src/cache.ts | 3 +- yarn.lock | 10 +++---- 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 9b738817..8d00e090 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "cross-fetch": "3.1.4", "dotenv": "10.0.0", "emittery": "0.8.1", - "type-fest": "1.2.1" + "type-fest": "^2.12.0" }, "size-limit": [ { diff --git a/src/cache.spec.ts b/src/cache.spec.ts index 827370e1..651355bf 100644 --- a/src/cache.spec.ts +++ b/src/cache.spec.ts @@ -1,8 +1,17 @@ +import { PartialDeep } from "type-fest"; + import { CachedFetcher } from "./cache"; import { Context } from "./context"; const contextCacheSpy = jest.spyOn(Context.prototype, "cache", "get"); const currentValueSpy = jest.spyOn(CachedFetcher.prototype, "currentValue", "get"); +const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); +const fetchWithTimeoutSpy: jest.SpyInstance>> = jest.spyOn( + CachedFetcher.prototype, + "fetchWithTimeout" +); +const nowMock = new Date(4242, 4, 2).getTime(); +jest.spyOn(Date.prototype, "getTime").mockReturnValue(nowMock); describe("CachedFetcher", () => { let cachedFetcher: CachedFetcher; @@ -46,12 +55,77 @@ describe("CachedFetcher", () => { }); it("should return cached when there is a current value", async () => { - currentValueSpy.mockReturnValue({ cachedValue: true }); + currentValueSpy.mockReturnValueOnce({ cachedValue: true }); const actualFetch = await cachedFetcher.fetch(); expect(actualFetch).toEqual({ cachedValue: true }); }); + + it("should return the JSON", async () => { + const responseMock: PartialDeep = { + status: 200, + headers: { + get: _ => null + }, + json: () => + Promise.resolve({ + foo: "bar" + }) + }; + fetchWithTimeoutSpy.mockResolvedValueOnce(responseMock); + + const actualFetch = await cachedFetcher.fetch("fooParam"); + + expect(fetchWithTimeoutSpy).toHaveBeenCalledWith("url/v1/chains/1/path?fooParam", 5000); + expect(actualFetch).toEqual({ + foo: "bar" + }); + + expect(cachedFetcher.expiryDate).toEqual(new Date(nowMock + 30 * 1000)); + expect(cachedFetcher.cachedValue).toEqual({ + foo: "bar" + }); + }); + + it("should log warning when `fetchWithTimeout` fails", async () => { + fetchWithTimeoutSpy.mockImplementation(() => { + throw new Error("fetchWithTimeout failed!"); + }); + + const actualFetch = await cachedFetcher.fetch("fooParam"); + + expect(consoleWarnSpy).toHaveBeenCalledWith("Call to cache at url/v1/chains/1/path?fooParam timed out"); + expect(actualFetch).toEqual(undefined); + }); + + it("should log warning when `status` is not 200", async () => { + const responseMock: PartialDeep = { + status: 42, + url: "url42", + statusText: "ultimate question of life, the universe, and everything" + }; + fetchWithTimeoutSpy.mockResolvedValueOnce(responseMock); + + const actualFetch = await cachedFetcher.fetch("fooParam"); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Call to cache failed at url42 (status 42 ultimate question of life, the universe, and everything)" + ); + expect(actualFetch).toEqual(undefined); + }); + + it("should return when there's no JSON", async () => { + const responseMock: PartialDeep = { + status: 200, + json: () => Promise.resolve(null) + }; + fetchWithTimeoutSpy.mockResolvedValueOnce(responseMock); + + const actualFetch = await cachedFetcher.fetch("fooParam"); + + expect(actualFetch).toEqual(undefined); + }); }); }); }); diff --git a/src/cache.ts b/src/cache.ts index bf735a02..30e723c8 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -37,6 +37,7 @@ export class CachedFetcher { console.warn(`Call to cache at ${path} timed out`); return undefined; } + if (call.status !== 200) { const { url, status, statusText } = call; console.warn(`Call to cache failed at ${url} (status ${status} ${statusText})`); @@ -57,7 +58,7 @@ export class CachedFetcher { return json as T; } - private async fetchWithTimeout(url: string, timeout: number): Promise { + async fetchWithTimeout(url: string, timeout: number): Promise { return Promise.race([ fetch(url), new Promise((_, reject) => setTimeout(() => reject("timeout"), timeout)) diff --git a/yarn.lock b/yarn.lock index 2826f5dc..b7f41afd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6637,11 +6637,6 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.2.1.tgz#232990aa513f3f5223abf54363975dfe3a121a2e" - integrity sha512-SbmIRuXhJs8KTneu77Ecylt9zuqL683tuiLYpTRil4H++eIhqCmx6ko6KAFem9dty8sOdnEiX7j4K1nRE628fQ== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -6657,6 +6652,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.0.tgz#ce342f58cab9114912f54b493d60ab39c3fc82b6" + integrity sha512-Qe5GRT+n/4GoqCNGGVp5Snapg1Omq3V7irBJB3EaKsp7HWDo5Gv2d/67gfNyV+d5EXD+x/RF5l1h4yJ7qNkcGA== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" From 5ae583054c08dbbe08f74f5f697bd1b8b760a039 Mon Sep 17 00:00:00 2001 From: Karelian Pie Date: Wed, 9 Mar 2022 15:19:02 +1100 Subject: [PATCH 3/5] fix: Cast prototype to any to allow for mocking private function/getter --- src/cache.spec.ts | 4 ++-- src/cache.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cache.spec.ts b/src/cache.spec.ts index 651355bf..e0a77220 100644 --- a/src/cache.spec.ts +++ b/src/cache.spec.ts @@ -4,10 +4,10 @@ import { CachedFetcher } from "./cache"; import { Context } from "./context"; const contextCacheSpy = jest.spyOn(Context.prototype, "cache", "get"); -const currentValueSpy = jest.spyOn(CachedFetcher.prototype, "currentValue", "get"); +const currentValueSpy = jest.spyOn(CachedFetcher.prototype as any, "currentValue", "get"); const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); const fetchWithTimeoutSpy: jest.SpyInstance>> = jest.spyOn( - CachedFetcher.prototype, + CachedFetcher.prototype as any, "fetchWithTimeout" ); const nowMock = new Date(4242, 4, 2).getTime(); diff --git a/src/cache.ts b/src/cache.ts index 30e723c8..b364def6 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -58,14 +58,14 @@ export class CachedFetcher { return json as T; } - async fetchWithTimeout(url: string, timeout: number): Promise { + private async fetchWithTimeout(url: string, timeout: number): Promise { return Promise.race([ fetch(url), new Promise((_, reject) => setTimeout(() => reject("timeout"), timeout)) ]); } - get currentValue(): T | undefined { + private get currentValue(): T | undefined { if (!this.expiryDate) { return undefined; } From 6183d26d1094fda458807d950607aa30a73848d6 Mon Sep 17 00:00:00 2001 From: Karelian Pie Date: Wed, 9 Mar 2022 15:20:53 +1100 Subject: [PATCH 4/5] refactor: Remove new line --- src/cache.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cache.ts b/src/cache.ts index b364def6..fc48fd4d 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -37,7 +37,6 @@ export class CachedFetcher { console.warn(`Call to cache at ${path} timed out`); return undefined; } - if (call.status !== 200) { const { url, status, statusText } = call; console.warn(`Call to cache failed at ${url} (status ${status} ${statusText})`); From 6868d1868d41bb1255bb79bb0cfa990966e93730 Mon Sep 17 00:00:00 2001 From: Karelian Pie Date: Thu, 10 Mar 2022 09:19:56 +1100 Subject: [PATCH 5/5] build: Downgrade type-fest --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8d00e090..9b738817 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "cross-fetch": "3.1.4", "dotenv": "10.0.0", "emittery": "0.8.1", - "type-fest": "^2.12.0" + "type-fest": "1.2.1" }, "size-limit": [ { diff --git a/yarn.lock b/yarn.lock index b7f41afd..2826f5dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6637,6 +6637,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.2.1.tgz#232990aa513f3f5223abf54363975dfe3a121a2e" + integrity sha512-SbmIRuXhJs8KTneu77Ecylt9zuqL683tuiLYpTRil4H++eIhqCmx6ko6KAFem9dty8sOdnEiX7j4K1nRE628fQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -6652,11 +6657,6 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.0.tgz#ce342f58cab9114912f54b493d60ab39c3fc82b6" - integrity sha512-Qe5GRT+n/4GoqCNGGVp5Snapg1Omq3V7irBJB3EaKsp7HWDo5Gv2d/67gfNyV+d5EXD+x/RF5l1h4yJ7qNkcGA== - typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"