Skip to content

Commit 86cfe2a

Browse files
committed
Add tests for asset upload utility method
1 parent 4f77dfb commit 86cfe2a

File tree

4 files changed

+238
-26
lines changed

4 files changed

+238
-26
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"format": "prettier . --write --ignore-unknown",
1010
"build": "tsc",
1111
"prepack": "cp -rv dist/. .",
12-
"test": "jest"
12+
"test": "jest tests/wrapper/AssetsUtilitiesClient.test.ts"
1313
},
1414
"dependencies": {
1515
"crypto-browserify": "^3.12.1",
@@ -30,6 +30,7 @@
3030
"@types/url-join": "4.0.1",
3131
"jest": "29.7.0",
3232
"jest-environment-jsdom": "29.7.0",
33+
"jest-fetch-mock": "^3.0.3",
3334
"prettier": "2.7.1",
3435
"ts-jest": "29.1.1",
3536
"ts-loader": "^9.3.1",

src/wrapper/AssetsUtilitiesClient.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import urlJoin from "url-join";
21
import * as Webflow from "../api";
32
import { Assets } from "../api/resources/assets/client/Client";
43
import * as core from "../core";
@@ -27,17 +26,12 @@ export declare namespace AssetsUtilities {
2726

2827
interface TestAssetUploadRequest {
2928
/**
30-
* File to upload via various formats
29+
* File to upload via URL where the asset is hosted, or by ArrayBuffer
3130
*/
3231
file: ArrayBuffer | string;
3332

3433
/**
35-
* The file MIME type
36-
*/
37-
mimeType?: string;
38-
39-
/**
40-
* Name of the file
34+
* Name of the file to upload, including the extension
4135
*/
4236
fileName: string;
4337

@@ -54,9 +48,15 @@ export class Client extends Assets {
5448
}
5549

5650
private async _getBufferFromUrl(url: string): Promise<ArrayBuffer> {
57-
const response = await fetch(url);
58-
const buffer = await response.arrayBuffer();
59-
return buffer;
51+
try {
52+
const response = await fetch(url);
53+
if (!response.ok) {
54+
throw new Error(`Failed to fetch asset from URL: ${url}. Status: ${response.status} ${response.statusText}`);
55+
}
56+
return await response.arrayBuffer();
57+
} catch (error) {
58+
throw new Error(`Error occurred while fetching asset from URL: ${url}. ${(error as Error).message}`);
59+
}
6060
}
6161

6262
/**
@@ -73,7 +73,7 @@ export class Client extends Assets {
7373
requestOptions?: Assets.RequestOptions
7474
): Promise<Webflow.AssetUpload> {
7575
/** 1. Generate the hash */
76-
const file = request.file;
76+
const {file, fileName, parentFolder} = request;
7777
let tempBuffer: Buffer | null = null;
7878
if (typeof file === 'string') {
7979
const arrBuffer = await this._getBufferFromUrl(file);
@@ -85,21 +85,25 @@ export class Client extends Assets {
8585
throw new Error('Invalid file');
8686
}
8787
const hash = crypto.createHash("md5").update(Buffer.from(tempBuffer)).digest("hex");
88-
const fileName = request.fileName;
8988

9089
const wfUploadRequest = {
9190
fileName,
9291
fileHash: hash,
93-
};
92+
} as Webflow.AssetsCreateRequest;
93+
if (parentFolder) {
94+
wfUploadRequest["parentFolder"] = parentFolder;
95+
}
96+
9497

9598
/** 2. Create the Asset Metadata in Webflow */
96-
const createWfAssetMetadata = async () => {
97-
return await this.create(siteId, wfUploadRequest, requestOptions);
98-
};
99+
let wfUploadedAsset: Webflow.AssetUpload;
100+
try {
101+
wfUploadedAsset = await this.create(siteId, wfUploadRequest, requestOptions);
102+
} catch (error) {
103+
throw new Error(`Failed to create Asset metadata in Webflow: ${(error as Error).message}`);
104+
}
99105

100106
/** 3. Create FormData with S3 bucket signature */
101-
const wfUploadedAsset = await createWfAssetMetadata();
102-
103107
const wfUploadDetails = wfUploadedAsset.uploadDetails!;
104108
const uploadUrl = wfUploadedAsset.uploadUrl as string;
105109
// Temp workaround since headers from response are being camelCased and we need them to be exact when sending to S3
@@ -132,11 +136,20 @@ export class Client extends Assets {
132136
});
133137

134138
/** 4. Upload to S3 */
135-
await fetch(uploadUrl, {
136-
method: 'POST',
137-
body: formDataToUpload,
138-
headers: {...formDataToUpload.getHeaders()},
139-
});
139+
try {
140+
const response = await fetch(uploadUrl, {
141+
method: 'POST',
142+
body: formDataToUpload,
143+
headers: { ...formDataToUpload.getHeaders() },
144+
});
145+
146+
if (!response.ok) {
147+
const errorText = await response.text();
148+
throw new Error(`Failed to upload to S3. Status: ${response.status}, Response: ${errorText}`);
149+
}
150+
} catch (error) {
151+
throw new Error(`Error occurred during S3 upload: ${(error as Error).message}`);
152+
}
140153

141154
return wfUploadedAsset;
142155
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
require('jest-fetch-mock').enableMocks();
2+
import { Client as AssetsUtilitiesClient } from "../../src/wrapper/AssetsUtilitiesClient";
3+
import * as Webflow from "../../src/api";
4+
import fetchMock from "jest-fetch-mock";
5+
import crypto from "crypto";
6+
import FormDataConstructor from 'form-data';
7+
8+
fetchMock.enableMocks();
9+
10+
describe("AssetsUtilitiesClient", () => {
11+
const mockOptions = {
12+
environment: () => "test-environment",
13+
accessToken: () => "test-access-token",
14+
};
15+
16+
const siteId = "test-site-id";
17+
const mockUploadUrl = "https://mock-s3-upload-url.com";
18+
const mockFileName = "test-file.txt";
19+
const mockFileContent = "Hello, world!";
20+
const mockFileBuffer = Buffer.from(mockFileContent);
21+
const mockFileHash = crypto.createHash("md5").update(mockFileBuffer).digest("hex");
22+
23+
let client: AssetsUtilitiesClient;
24+
25+
beforeEach(() => {
26+
fetchMock.resetMocks();
27+
client = new AssetsUtilitiesClient(mockOptions);
28+
});
29+
30+
it("should throw an error if it cannot fetch the asset successfully", async () => {
31+
const invalidUrl = "https://invalid-url.com";
32+
33+
// Mock the fetch response to simulate a failure
34+
fetchMock.mockResponseOnce("", { status: 404, statusText: "Not Found" });
35+
36+
await expect(client["_getBufferFromUrl"](invalidUrl)).rejects.toThrow(
37+
"Failed to fetch asset from URL: https://invalid-url.com. Status: 404 Not Found"
38+
);
39+
40+
// Ensure fetch was called with the correct URL
41+
expect(fetchMock).toHaveBeenCalledWith(invalidUrl);
42+
});
43+
44+
it("should throw an error for invalid file input", async () => {
45+
await expect(client.createAndUpload(siteId, {
46+
fileName: mockFileName,
47+
file: null as unknown as ArrayBuffer, // Invalid file
48+
})).rejects.toThrow("Invalid file");
49+
});
50+
51+
it("should throw an error if it fails to create Webflow Asset metadata", async () => {
52+
// Mock the Webflow API to throw an error
53+
jest.spyOn(client, "create").mockRejectedValue(new Error("Webflow API error"));
54+
55+
await expect(client.createAndUpload(siteId, {
56+
fileName: mockFileName,
57+
file: mockFileBuffer.buffer, // Pass ArrayBuffer
58+
})).rejects.toThrow("Failed to create Asset metadata in Webflow: Webflow API error");
59+
60+
// Ensure the create method was called
61+
expect(client.create).toHaveBeenCalledWith(siteId, expect.objectContaining({
62+
fileName: mockFileName,
63+
fileHash: expect.any(String),
64+
}), undefined);
65+
});
66+
67+
it("should throw an error if it fails to upload to S3", async () => {
68+
// Mock the Webflow API response for creating asset metadata
69+
const mockCreateResponse = {
70+
uploadUrl: mockUploadUrl,
71+
uploadDetails: {
72+
"xAmzAlgorithm": "AWS4-HMAC-SHA256",
73+
"xAmzDate": "20231010T000000Z",
74+
"xAmzCredential": "mock-credential",
75+
"xAmzSignature": "mock-signature",
76+
"successActionStatus": "201",
77+
"contentType": "text/plain",
78+
},
79+
};
80+
jest.spyOn(client, "create").mockResolvedValue(mockCreateResponse as Webflow.AssetUpload);
81+
82+
// Mock the S3 upload response to fail
83+
fetchMock.mockResponseOnce("S3 upload error", { status: 500 });
84+
85+
await expect(client.createAndUpload(siteId, {
86+
fileName: mockFileName,
87+
file: mockFileBuffer.buffer, // Pass ArrayBuffer
88+
})).rejects.toThrow("Failed to upload to S3. Status: 500, Response: S3 upload error");
89+
90+
// Ensure the S3 upload was attempted
91+
expect(fetchMock).toHaveBeenCalledWith(mockUploadUrl, expect.objectContaining({
92+
method: "POST",
93+
body: expect.any(FormDataConstructor),
94+
}));
95+
});
96+
97+
it("should create and upload a file from an ArrayBuffer", async () => {
98+
// Mock the Webflow API response for creating asset metadata
99+
const mockCreateResponse = {
100+
uploadUrl: mockUploadUrl,
101+
uploadDetails: {
102+
"xAmzAlgorithm": "AWS4-HMAC-SHA256",
103+
"xAmzDate": "20231010T000000Z",
104+
"xAmzCredential": "mock-credential",
105+
"xAmzSignature": "mock-signature",
106+
"successActionStatus": "201",
107+
"contentType": "text/plain",
108+
},
109+
};
110+
jest.spyOn(client, "create").mockResolvedValue(mockCreateResponse as Webflow.AssetUpload);
111+
112+
// Mock the S3 upload response
113+
fetchMock.mockResponseOnce(JSON.stringify({ success: true }), { status: 201 });
114+
115+
const result = await client.createAndUpload(siteId, {
116+
fileName: mockFileName,
117+
file: mockFileBuffer.buffer, // Pass ArrayBuffer
118+
});
119+
120+
// Assertions
121+
expect(client.create).toHaveBeenCalledWith(siteId, expect.objectContaining({
122+
fileName: mockFileName,
123+
fileHash: expect.any(String),
124+
}), undefined);
125+
126+
expect(fetchMock).toHaveBeenCalledWith(mockUploadUrl, expect.objectContaining({
127+
method: "POST",
128+
body: expect.any(FormDataConstructor),
129+
}));
130+
131+
expect(result).toEqual(mockCreateResponse);
132+
});
133+
134+
it("should create and upload a file from a URL", async () => {
135+
// Mock the file fetch response (first fetch call)
136+
fetchMock.mockResponseOnce(mockFileContent);
137+
138+
// Mock the Webflow API response for creating asset metadata
139+
const mockCreateResponse = {
140+
uploadUrl: mockUploadUrl,
141+
uploadDetails: {
142+
"xAmzAlgorithm": "AWS4-HMAC-SHA256",
143+
"xAmzDate": "20231010T000000Z",
144+
"xAmzCredential": "mock-credential",
145+
"xAmzSignature": "mock-signature",
146+
"successActionStatus": "201",
147+
"contentType": "text/plain",
148+
},
149+
};
150+
jest.spyOn(client, "create").mockResolvedValue(mockCreateResponse as Webflow.AssetUpload);
151+
152+
// Mock the S3 upload response (second fetch call)
153+
fetchMock.mockResponseOnce(JSON.stringify({ success: true }), { status: 201 });
154+
155+
const result = await client.createAndUpload(siteId, {
156+
fileName: mockFileName,
157+
file: "https://mock-file-url.com", // Pass asset URL
158+
});
159+
160+
// Assertions for the file fetch
161+
expect(fetchMock).toHaveBeenNthCalledWith(1, "https://mock-file-url.com");
162+
163+
// Assertions for the Webflow API call
164+
expect(client.create).toHaveBeenCalledWith(siteId, {
165+
fileName: mockFileName,
166+
fileHash: mockFileHash,
167+
}, undefined);
168+
169+
// Assertions for the S3 upload
170+
expect(fetchMock).toHaveBeenNthCalledWith(2, mockUploadUrl, expect.objectContaining({
171+
method: "POST",
172+
body: expect.any(FormDataConstructor),
173+
}));
174+
175+
expect(result).toEqual(mockCreateResponse);
176+
});
177+
});
178+

yarn.lock

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,13 @@ create-jest@^29.7.0:
13291329
jest-util "^29.7.0"
13301330
prompts "^2.0.1"
13311331

1332+
cross-fetch@^3.0.4:
1333+
version "3.2.0"
1334+
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3"
1335+
integrity sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==
1336+
dependencies:
1337+
node-fetch "^2.7.0"
1338+
13321339
cross-spawn@^7.0.3:
13331340
version "7.0.6"
13341341
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -2144,6 +2151,14 @@ jest-environment-node@^29.7.0:
21442151
jest-mock "^29.7.0"
21452152
jest-util "^29.7.0"
21462153

2154+
jest-fetch-mock@^3.0.3:
2155+
version "3.0.3"
2156+
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
2157+
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
2158+
dependencies:
2159+
cross-fetch "^3.0.4"
2160+
promise-polyfill "^8.1.3"
2161+
21472162
jest-get-type@^29.6.3:
21482163
version "29.6.3"
21492164
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
@@ -2603,7 +2618,7 @@ neo-async@^2.6.2:
26032618
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
26042619
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
26052620

2606-
2621+
[email protected], node-fetch@^2.7.0:
26072622
version "2.7.0"
26082623
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
26092624
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@@ -2788,6 +2803,11 @@ process@^0.11.10:
27882803
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
27892804
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
27902805

2806+
promise-polyfill@^8.1.3:
2807+
version "8.3.0"
2808+
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
2809+
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
2810+
27912811
prompts@^2.0.1:
27922812
version "2.4.2"
27932813
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"

0 commit comments

Comments
 (0)