From e1286eb635f9d5bd7da7c4f1d6af38b6edb30a8f Mon Sep 17 00:00:00 2001 From: iammatthias Date: Tue, 1 Jul 2025 09:59:08 -0700 Subject: [PATCH 1/5] fix: Add onrejected callback to then method in OptimizeImage classes This update enhances the then method in both OptimizeImageCreateSignedUrl and OptimizeImageGetCid classes by adding an optional onrejected callback to handle promise rejections. --- .../gateways/OptimizeImageCreateSignedUrl.ts | 6 +- .../classes/gateways/OptimizeImageGetCid.ts | 6 +- .../optimizeImagePromiseHandling.test.ts | 198 ++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 tests/gateway/optimizeImagePromiseHandling.test.ts diff --git a/src/core/classes/gateways/OptimizeImageCreateSignedUrl.ts b/src/core/classes/gateways/OptimizeImageCreateSignedUrl.ts index 45f25e6..472ce40 100644 --- a/src/core/classes/gateways/OptimizeImageCreateSignedUrl.ts +++ b/src/core/classes/gateways/OptimizeImageCreateSignedUrl.ts @@ -20,9 +20,13 @@ export class OptimizeImageCreateAccessLink { return this; } - then(onfulfilled?: ((value: string) => any) | null): Promise { + then( + onfulfilled?: ((value: string) => any) | null, + onrejected?: ((reason: any) => any) | null, + ): Promise { return createAccessLink(this.config, this.urlOpts, this.imgOpts).then( onfulfilled, + onrejected, ); } } diff --git a/src/core/classes/gateways/OptimizeImageGetCid.ts b/src/core/classes/gateways/OptimizeImageGetCid.ts index 5ff53dc..d7bfb5d 100644 --- a/src/core/classes/gateways/OptimizeImageGetCid.ts +++ b/src/core/classes/gateways/OptimizeImageGetCid.ts @@ -26,9 +26,13 @@ export class OptimizeImageGetCid { return this; } - then(onfulfilled?: ((value: GetCIDResponse) => any) | null): Promise { + then( + onfulfilled?: ((value: GetCIDResponse) => any) | null, + onrejected?: ((reason: any) => any) | null, + ): Promise { return getCid(this.config, this.cid, this.gatewayType, this.options).then( onfulfilled, + onrejected, ); } } diff --git a/tests/gateway/optimizeImagePromiseHandling.test.ts b/tests/gateway/optimizeImagePromiseHandling.test.ts new file mode 100644 index 0000000..73fb349 --- /dev/null +++ b/tests/gateway/optimizeImagePromiseHandling.test.ts @@ -0,0 +1,198 @@ +import { PinataSDK } from "../../src/core/pinataSDK"; +import type { PinataConfig } from "../../src/core/types"; +import { + PinataError, + NetworkError, + AuthenticationError, + ValidationError, +} from "../../src/utils/custom-errors"; + +describe("OptimizeImage Promise Handling", () => { + let originalFetch: typeof fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + const mockConfig: PinataConfig = { + pinataJwt: "test_jwt", + pinataGateway: "https://test.mypinata.cloud", + }; + + describe("OptimizeImageGetCid", () => { + it("should properly handle promise rejections when content is not pinned", async () => { + // Mock a 404 response to simulate content not being pinned + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 404, + text: jest.fn().mockResolvedValueOnce("Not Found"), + }); + + const pinata = new PinataSDK(mockConfig); + const cid = "QmcUNH54shLGC8tmkzt7ounwFc2W2bg3xLqbmdq5wHHj7R"; + + let errorCaught = false; + let errorMessage = ""; + + try { + await pinata.gateways.public.get(cid); + } catch (error) { + errorCaught = true; + errorMessage = error instanceof Error ? error.message : String(error); + } + + expect(errorCaught).toBe(true); + expect(errorMessage).toContain("HTTP error"); + }); + + it("should properly handle promise rejections with .then() and .catch()", async () => { + // Mock a 404 response to simulate content not being pinned + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 404, + text: jest.fn().mockResolvedValueOnce("Not Found"), + }); + + const pinata = new PinataSDK(mockConfig); + const cid = "QmcUNH54shLGC8tmkzt7ounwFc2W2bg3xLqbmdq5wHHj7R"; + + let tryBlockExecuted = false; + let catchBlockExecuted = false; + let doneBlockExecuted = false; + let errorBlockExecuted = false; + + try { + await pinata.gateways.public.get(cid); + tryBlockExecuted = true; + } catch { + catchBlockExecuted = true; + } + + // Test the promise chain behavior + await pinata.gateways.public + .get(cid) + .then(() => { + doneBlockExecuted = true; + }) + .catch(() => { + errorBlockExecuted = true; + }); + + expect(tryBlockExecuted).toBe(false); + expect(catchBlockExecuted).toBe(true); + expect(doneBlockExecuted).toBe(false); + expect(errorBlockExecuted).toBe(true); + }); + + it("should properly handle promise rejections with image optimization", async () => { + // Mock a 404 response to simulate content not being pinned + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 404, + text: jest.fn().mockResolvedValueOnce("Not Found"), + }); + + const pinata = new PinataSDK(mockConfig); + const cid = "QmcUNH54shLGC8tmkzt7ounwFc2W2bg3xLqbmdq5wHHj7R"; + + let errorCaught = false; + + try { + await pinata.gateways.public + .get(cid) + .optimizeImage({ width: 100, height: 100 }); + } catch (error) { + errorCaught = true; + } + + expect(errorCaught).toBe(true); + }); + + it("should properly handle authentication errors", async () => { + // Mock a 401 response to simulate authentication error + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 401, + text: jest.fn().mockResolvedValueOnce("Unauthorized"), + }); + + const pinata = new PinataSDK(mockConfig); + const cid = "QmcUNH54shLGC8tmkzt7ounwFc2W2bg3xLqbmdq5wHHj7R"; + + let errorCaught = false; + let isAuthenticationError = false; + + try { + await pinata.gateways.public.get(cid); + } catch (error) { + errorCaught = true; + isAuthenticationError = error instanceof AuthenticationError; + } + + expect(errorCaught).toBe(true); + expect(isAuthenticationError).toBe(true); + }); + }); + + describe("OptimizeImageCreateAccessLink", () => { + it("should properly handle promise rejections", async () => { + // Mock a 401 response to simulate authentication error + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 401, + text: jest.fn().mockResolvedValueOnce("Unauthorized"), + }); + + const pinata = new PinataSDK(mockConfig); + const options = { + cid: "QmcUNH54shLGC8tmkzt7ounwFc2W2bg3xLqbmdq5wHHj7R", + expires: 3600, + }; + + let errorCaught = false; + let isAuthenticationError = false; + + try { + await pinata.gateways.private.createAccessLink(options); + } catch (error) { + errorCaught = true; + isAuthenticationError = error instanceof AuthenticationError; + } + + expect(errorCaught).toBe(true); + expect(isAuthenticationError).toBe(true); + }); + + it("should properly handle promise rejections with image optimization", async () => { + // Mock a 401 response to simulate authentication error + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + status: 401, + text: jest.fn().mockResolvedValueOnce("Unauthorized"), + }); + + const pinata = new PinataSDK(mockConfig); + const options = { + cid: "QmcUNH54shLGC8tmkzt7ounwFc2W2bg3xLqbmdq5wHHj7R", + expires: 3600, + }; + + let errorCaught = false; + + try { + await pinata.gateways.private + .createAccessLink(options) + .optimizeImage({ width: 100, height: 100 }); + } catch (error) { + errorCaught = true; + } + + expect(errorCaught).toBe(true); + }); + }); +}); \ No newline at end of file From 78907d5090e21fd2abe14b3762646ed16bd8fca6 Mon Sep 17 00:00:00 2001 From: iammatthias Date: Tue, 1 Jul 2025 10:31:13 -0700 Subject: [PATCH 2/5] fix: Add onrejected callback to then method in various analytics and file filter classes This update enhances the then method in multiple classes, including AnalyticsBuilder, AnalyticsFilter, FilterFiles, FilterQueue, GroupsFilter, and FilterKeys, by adding an optional onrejected callback to handle promise rejections. --- src/core/classes/analytics/AnalyticsBuilder.ts | 7 +++++-- src/core/classes/analytics/AnalyticsFilter.ts | 3 ++- src/core/classes/files/FilterFiles.ts | 7 +++++-- src/core/classes/files/FilterQueue.ts | 7 +++++-- src/core/classes/groups/GroupsFilter.ts | 3 ++- src/core/classes/keys/FilterKeys.ts | 7 +++++-- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/core/classes/analytics/AnalyticsBuilder.ts b/src/core/classes/analytics/AnalyticsBuilder.ts index fe82f39..d03018f 100644 --- a/src/core/classes/analytics/AnalyticsBuilder.ts +++ b/src/core/classes/analytics/AnalyticsBuilder.ts @@ -72,7 +72,10 @@ export class AnalyticsBuilder { throw new Error("getAnalytics method must be implemented in derived class"); } - then(onfulfilled?: ((value: R) => any) | null): Promise { - return this.getAnalytics().then(onfulfilled); + then( + onfulfilled?: ((value: R) => any) | null, + onrejected?: ((reason: any) => any) | null, + ): Promise { + return this.getAnalytics().then(onfulfilled, onrejected); } } diff --git a/src/core/classes/analytics/AnalyticsFilter.ts b/src/core/classes/analytics/AnalyticsFilter.ts index 78f6538..d96df8f 100644 --- a/src/core/classes/analytics/AnalyticsFilter.ts +++ b/src/core/classes/analytics/AnalyticsFilter.ts @@ -93,7 +93,8 @@ export class AnalyticsFilter { then( onfulfilled?: ((value: TopAnalyticsResponse) => any) | null, + onrejected?: ((reason: any) => any) | null, ): Promise { - return analyticsTopUsage(this.config, this.query).then(onfulfilled); + return analyticsTopUsage(this.config, this.query).then(onfulfilled, onrejected); } } diff --git a/src/core/classes/files/FilterFiles.ts b/src/core/classes/files/FilterFiles.ts index e2cede8..c6063e2 100644 --- a/src/core/classes/files/FilterFiles.ts +++ b/src/core/classes/files/FilterFiles.ts @@ -72,8 +72,11 @@ export class FilterFiles { return this; } - then(onfulfilled?: ((value: FileListResponse) => any) | null): Promise { - return this.fetchPage().then(onfulfilled); + then( + onfulfilled?: ((value: FileListResponse) => any) | null, + onrejected?: ((reason: any) => any) | null, + ): Promise { + return this.fetchPage().then(onfulfilled, onrejected); } private async fetchPage(): Promise { diff --git a/src/core/classes/files/FilterQueue.ts b/src/core/classes/files/FilterQueue.ts index 7f1556c..38f25fa 100644 --- a/src/core/classes/files/FilterQueue.ts +++ b/src/core/classes/files/FilterQueue.ts @@ -64,8 +64,11 @@ export class FilterQueue { return this; } - then(onfulfilled?: ((value: PinQueueResponse) => any) | null): Promise { - return queue(this.config, this.query).then(onfulfilled); + then( + onfulfilled?: ((value: PinQueueResponse) => any) | null, + onrejected?: ((reason: any) => any) | null, + ): Promise { + return queue(this.config, this.query).then(onfulfilled, onrejected); } // rate limit, hopefully temporary? diff --git a/src/core/classes/groups/GroupsFilter.ts b/src/core/classes/groups/GroupsFilter.ts index 4dde104..8ebf3e0 100644 --- a/src/core/classes/groups/GroupsFilter.ts +++ b/src/core/classes/groups/GroupsFilter.ts @@ -44,13 +44,14 @@ export class FilterGroups { then( onfulfilled?: ((value: GroupListResponse) => any) | null, + onrejected?: ((reason: any) => any) | null, ): Promise { return this.fetchPage() .then((response) => { this.nextPageToken = response.next_page_token; return response; }) - .then(onfulfilled); + .then(onfulfilled, onrejected); } private async fetchPage(): Promise { diff --git a/src/core/classes/keys/FilterKeys.ts b/src/core/classes/keys/FilterKeys.ts index 6610a44..3a76339 100644 --- a/src/core/classes/keys/FilterKeys.ts +++ b/src/core/classes/keys/FilterKeys.ts @@ -39,8 +39,11 @@ export class FilterKeys { return this; } - then(onfulfilled?: ((value: KeyListItem[]) => any) | null): Promise { - return listKeys(this.config, this.query).then(onfulfilled); + then( + onfulfilled?: ((value: KeyListItem[]) => any) | null, + onrejected?: ((reason: any) => any) | null, + ): Promise { + return listKeys(this.config, this.query).then(onfulfilled, onrejected); } // private async rateLimit(): Promise { From 06a2425f8da99661623230ad9404c3efe906ce75 Mon Sep 17 00:00:00 2001 From: iammatthias Date: Tue, 1 Jul 2025 11:32:27 -0700 Subject: [PATCH 3/5] chore: Set Biome version in code quality workflow to 1.9.4 --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 882aeb6..86082ef 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Biome uses: biomejs/setup-biome@v2 with: - version: latest + version: 1.9.4 - name: Run Biome run: biome ci --formatter-enabled=true --linter-enabled=false --organize-imports-enabled=false src tests tests: From cba3c49f97f24b49e4ea8545737d37395055ec21 Mon Sep 17 00:00:00 2001 From: iammatthias Date: Tue, 1 Jul 2025 11:34:23 -0700 Subject: [PATCH 4/5] refactor: Format promise handling to make Biome happy --- src/core/classes/analytics/AnalyticsFilter.ts | 5 ++++- tests/gateway/optimizeImagePromiseHandling.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/classes/analytics/AnalyticsFilter.ts b/src/core/classes/analytics/AnalyticsFilter.ts index d96df8f..b9a4987 100644 --- a/src/core/classes/analytics/AnalyticsFilter.ts +++ b/src/core/classes/analytics/AnalyticsFilter.ts @@ -95,6 +95,9 @@ export class AnalyticsFilter { onfulfilled?: ((value: TopAnalyticsResponse) => any) | null, onrejected?: ((reason: any) => any) | null, ): Promise { - return analyticsTopUsage(this.config, this.query).then(onfulfilled, onrejected); + return analyticsTopUsage(this.config, this.query).then( + onfulfilled, + onrejected, + ); } } diff --git a/tests/gateway/optimizeImagePromiseHandling.test.ts b/tests/gateway/optimizeImagePromiseHandling.test.ts index 73fb349..2905667 100644 --- a/tests/gateway/optimizeImagePromiseHandling.test.ts +++ b/tests/gateway/optimizeImagePromiseHandling.test.ts @@ -195,4 +195,4 @@ describe("OptimizeImage Promise Handling", () => { expect(errorCaught).toBe(true); }); }); -}); \ No newline at end of file +}); From ebca38baffb1bcfa0a53f6d473f0dd64bb863d7b Mon Sep 17 00:00:00 2001 From: iammatthias Date: Tue, 1 Jul 2025 11:52:39 -0700 Subject: [PATCH 5/5] chore: bump version to 2.4.9 for bug fix release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ba665b..56819c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pinata", - "version": "2.4.8", + "version": "2.4.9", "description": "The official Pinata SDK", "main": "./dist/index.js", "module": "./dist/index.mjs",