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: 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", 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..b9a4987 100644 --- a/src/core/classes/analytics/AnalyticsFilter.ts +++ b/src/core/classes/analytics/AnalyticsFilter.ts @@ -93,7 +93,11 @@ 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/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/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 { diff --git a/tests/gateway/optimizeImagePromiseHandling.test.ts b/tests/gateway/optimizeImagePromiseHandling.test.ts new file mode 100644 index 0000000..2905667 --- /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); + }); + }); +});