Skip to content
This repository was archived by the owner on Sep 1, 2024. It is now read-only.

Commit 87ed590

Browse files
committed
Upload test runs to presigned S3 URLs
This change leverages a recent backend API change to support larger test runs. Inline test run submissions are subject to a 6MB due to AWS Lambda's payload limits.
1 parent 4daf067 commit 87ed590

File tree

3 files changed

+153
-44
lines changed

3 files changed

+153
-44
lines changed

packages/jest-plugin/test/integration/src/runTestCase.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
import { run } from "jest";
1313
import escapeStringRegexp from "escape-string-regexp";
1414
import {
15-
CreateTestSuiteRunRequest,
15+
CreateTestSuiteRunFromUploadRequest,
16+
CreateTestSuiteRunInlineRequest,
1617
TEST_NAME_ENTRY_MAX_LENGTH,
1718
TestAttemptResult,
1819
TestRunAttemptRecord,
@@ -26,6 +27,7 @@ import deepEqual from "deep-equal";
2627
import * as cosmiconfig from "cosmiconfig";
2728
import { CosmiconfigResult } from "cosmiconfig/dist/types";
2829
import { UnflakableConfig } from "../../../src/types";
30+
import { gunzipSync } from "zlib";
2931

3032
const userAgentRegex = new RegExp(
3133
"unflakable-js-api/(?:[-0-9.]|alpha|beta)+ unflakable-jest-plugin/(?:[-0-9.]|alpha|beta)+ \\(Jest [0-9]+\\.[0-9]+\\.[0-9]+; Node v[0-9]+\\.[0-9]+\\.[0-9]\\)"
@@ -487,7 +489,9 @@ const uploadResultsMatcher =
487489
results: ResultCounts
488490
): MockMatcher =>
489491
(_url, { body, headers }) => {
490-
const parsedBody = JSON.parse(body as string) as CreateTestSuiteRunRequest;
492+
const parsedBody = JSON.parse(
493+
gunzipSync(body as string).toString()
494+
) as CreateTestSuiteRunInlineRequest;
491495

492496
expect((headers as { [key in string]: string })["User-Agent"]).toMatch(
493497
userAgentRegex
@@ -802,26 +806,77 @@ const addFetchMockExpectations = (
802806
}
803807
);
804808
if (expectResultsToBeUploaded) {
809+
const uploadUrl =
810+
"https://s3.mock.amazonaws.com/unflakable-backend-mock-test-uploads/teams/MOCK_TEAM_ID/" +
811+
`suites/${expectedSuiteId}/runs/upload/MOCK_UPLOAD_ID?X-Amz-Signature=MOCK_SIGNATURE`;
805812
fetchMock.postOnce(
806813
{
807-
url: `https://app.unflakable.com/api/v1/test-suites/${expectedSuiteId}/runs`,
814+
url: `https://app.unflakable.com/api/v1/test-suites/${expectedSuiteId}/runs/upload`,
808815
headers: {
809816
Authorization: `Bearer ${expectedApiKey}`,
810817
"Content-Type": "application/json",
811818
},
819+
matcher: (_url, { body }) => {
820+
expect(body).toBe(undefined);
821+
return true;
822+
},
823+
},
824+
(): MockResponse => ({
825+
body: {
826+
upload_id: "MOCK_UPLOAD_ID",
827+
},
828+
headers: {
829+
Location: uploadUrl,
830+
},
831+
status: 201,
832+
})
833+
);
834+
835+
let runRequest: CreateTestSuiteRunInlineRequest | null = null;
836+
fetchMock.putOnce(
837+
{
838+
url: uploadUrl,
839+
headers: {
840+
"Content-Encoding": "gzip",
841+
"Content-Type": "application/json",
842+
},
812843
matcher: uploadResultsMatcher(params, results),
813844
},
814845
(_url: string, { body }: MockRequest): MockResponse => {
846+
runRequest = JSON.parse(
847+
gunzipSync(body as string).toString()
848+
) as CreateTestSuiteRunInlineRequest;
849+
850+
return {
851+
status: 200,
852+
};
853+
}
854+
);
855+
fetchMock.postOnce(
856+
{
857+
url: `https://app.unflakable.com/api/v1/test-suites/${expectedSuiteId}/runs`,
858+
headers: {
859+
Authorization: `Bearer ${expectedApiKey}`,
860+
"Content-Type": "application/json",
861+
},
862+
matcher: (_url, { body }) => {
863+
const parsedBody = JSON.parse(
864+
body as string
865+
) as CreateTestSuiteRunFromUploadRequest;
866+
expect(parsedBody.upload_id).toBe("MOCK_UPLOAD_ID");
867+
return true;
868+
},
869+
},
870+
(): MockResponse => {
871+
expect(runRequest).not.toBeNull();
872+
const parsedRequest = runRequest as CreateTestSuiteRunInlineRequest;
873+
815874
if (failToUploadResults) {
816875
return {
817876
throws: new Error("mock request failure"),
818877
};
819878
}
820879

821-
const parsedBody = JSON.parse(
822-
body as string
823-
) as CreateTestSuiteRunRequest;
824-
825880
return {
826881
body: {
827882
run_id: "MOCK_RUN_ID",
@@ -836,8 +891,8 @@ const addFetchMockExpectations = (
836891
commit: expectedCommit,
837892
}
838893
: {}),
839-
start_time: parsedBody.start_time,
840-
end_time: parsedBody.end_time,
894+
start_time: parsedRequest.start_time,
895+
end_time: parsedRequest.end_time,
841896
num_tests:
842897
results.failedTests +
843898
results.flakyTests +

packages/js-api/src/index.ts

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) 2022 Developer Innovations, LLC
22

3-
import fetch from "node-fetch";
3+
import fetch, { Response } from "node-fetch";
44
import _debug = require("debug");
5+
import { gzip } from "zlib";
6+
import { promisify } from "util";
57

68
const debug = _debug("unflakable:api");
79

@@ -34,13 +36,16 @@ export type TestRunRecord = {
3436
name: string[];
3537
attempts: TestRunAttemptRecord[];
3638
};
37-
export type CreateTestSuiteRunRequest = {
39+
export type CreateTestSuiteRunInlineRequest = {
3840
branch?: string;
3941
commit?: string;
4042
start_time: string;
4143
end_time: string;
4244
test_runs: TestRunRecord[];
4345
};
46+
export declare type CreateTestSuiteRunFromUploadRequest = {
47+
upload_id: string;
48+
};
4449
export type TestSuiteRunSummary = {
4550
run_id: string;
4651
suite_id: string;
@@ -54,52 +59,113 @@ export type TestSuiteRunSummary = {
5459
num_flake: number;
5560
num_quarantined: number;
5661
};
62+
export type CreateTestSuiteRunUploadUrlResponse = {
63+
upload_id: string;
64+
};
5765

5866
const userAgent = (clientDescription?: string) =>
5967
`unflakable-js-api/${JS_API_VERSION}${
6068
clientDescription !== undefined ? ` ${clientDescription}` : ""
6169
}`;
6270

71+
const requestHeaders = ({
72+
apiKey,
73+
clientDescription,
74+
}: {
75+
apiKey: string;
76+
clientDescription?: string;
77+
}) => ({
78+
Authorization: "Bearer " + apiKey,
79+
"User-Agent": userAgent(clientDescription),
80+
});
81+
82+
const expectResponse =
83+
(expectedStatus: number, expectedStatusText: string) =>
84+
async (res: Response): Promise<Response> => {
85+
if (res.status !== expectedStatus) {
86+
const body = await res.text();
87+
throw new Error(
88+
`received HTTP response \`${res.status} ${
89+
res.statusText
90+
}\` (expected \`${expectedStatusText}\`)${
91+
body.length > 0 ? `: ${body}` : ""
92+
}`
93+
);
94+
}
95+
return res;
96+
};
97+
6398
export const createTestSuiteRun = async ({
6499
request,
65100
testSuiteId,
66101
apiKey,
67102
clientDescription,
68103
baseUrl,
69104
}: {
70-
request: CreateTestSuiteRunRequest;
105+
request: CreateTestSuiteRunInlineRequest;
71106
testSuiteId: string;
72107
apiKey: string;
73108
clientDescription?: string;
74109
baseUrl?: string;
75110
}): Promise<TestSuiteRunSummary> => {
76111
const requestJson = JSON.stringify(request);
77112
debug(`Creating test suite run: ${requestJson}`);
78-
return await fetch(
113+
const gzippedRequest = await promisify(gzip)(requestJson);
114+
115+
const { uploadId, uploadUrl } = await fetch(
79116
`${
80117
baseUrl !== undefined ? baseUrl : BASE_URL
81-
}/api/v1/test-suites/${testSuiteId}/runs`,
118+
}/api/v1/test-suites/${testSuiteId}/runs/upload`,
82119
{
83120
method: "post",
84-
body: requestJson,
85121
headers: {
86-
Authorization: "Bearer " + apiKey,
87122
"Content-Type": "application/json",
88-
"User-Agent": userAgent(clientDescription),
123+
...requestHeaders({ apiKey, clientDescription }),
89124
},
90125
}
91126
)
127+
.then(expectResponse(201, "201 Created"))
92128
.then(async (res) => {
93-
if (res.status !== 201) {
94-
const body = await res.text();
95-
throw new Error(
96-
`received HTTP response \`${res.status} ${
97-
res.statusText
98-
}\` (expected \`201 Created\`)${body.length > 0 ? `: ${body}` : ""}`
99-
);
129+
const location = res.headers.get("Location");
130+
if (location === null) {
131+
throw new Error("no Location response header found");
100132
}
101-
return res.json() as Promise<TestSuiteRunSummary>;
102-
})
133+
const body = (await res.json()) as CreateTestSuiteRunUploadUrlResponse;
134+
return {
135+
uploadId: body.upload_id,
136+
uploadUrl: location,
137+
};
138+
});
139+
140+
await fetch(uploadUrl, {
141+
method: "put",
142+
body: gzippedRequest,
143+
headers: {
144+
"Content-Encoding": "gzip",
145+
"Content-Type": "application/json",
146+
"User-Agent": userAgent(clientDescription),
147+
},
148+
}).then(expectResponse(200, "200 OK"));
149+
150+
const requestBody: CreateTestSuiteRunFromUploadRequest = {
151+
upload_id: uploadId,
152+
};
153+
154+
return await fetch(
155+
`${
156+
baseUrl !== undefined ? baseUrl : BASE_URL
157+
}/api/v1/test-suites/${testSuiteId}/runs`,
158+
{
159+
method: "post",
160+
body: JSON.stringify(requestBody),
161+
headers: {
162+
"Content-Type": "application/json",
163+
...requestHeaders({ apiKey, clientDescription }),
164+
},
165+
}
166+
)
167+
.then(expectResponse(201, "201 Created"))
168+
.then((res) => res.json() as Promise<TestSuiteRunSummary>)
103169
.then((parsedResponse: TestSuiteRunSummary) => {
104170
debug(`Received response: ${JSON.stringify(parsedResponse)}`);
105171
return parsedResponse;
@@ -124,23 +190,11 @@ export const getTestSuiteManifest = async ({
124190
}/api/v1/test-suites/${testSuiteId}/manifest`,
125191
{
126192
method: "get",
127-
headers: {
128-
Authorization: "Bearer " + apiKey,
129-
"User-Agent": userAgent(clientDescription),
130-
},
193+
headers: requestHeaders({ apiKey, clientDescription }),
131194
}
132195
)
133-
.then(async (res) => {
134-
if (res.status !== 200) {
135-
const body = await res.text();
136-
throw new Error(
137-
`received HTTP response \`${res.status} ${
138-
res.statusText
139-
}\` (expected \`200 OK\`)${body.length > 0 ? `: ${body}` : ""}`
140-
);
141-
}
142-
return res.json() as Promise<TestSuiteManifest>;
143-
})
196+
.then(expectResponse(200, "200 OK"))
197+
.then((res) => res.json() as Promise<TestSuiteManifest>)
144198
.then((parsedResponse: TestSuiteManifest) => {
145199
debug(`Received response: ${JSON.stringify(parsedResponse)}`);
146200
return parsedResponse;

yarn.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2586,9 +2586,9 @@ __metadata:
25862586
linkType: hard
25872587

25882588
"caniuse-lite@npm:^1.0.30001313":
2589-
version: 1.0.30001313
2590-
resolution: "caniuse-lite@npm:1.0.30001313"
2591-
checksum: 49f2dcd1fa493a09a5247dcf3a4da3b9df355131b1fc1fd08b67ae7683c300ed9b9eef6a5424b4ac7e5d1ff0e129d2a0b4adf2a6a5a04ab5c2c0b2c590e935be
2589+
version: 1.0.30001390
2590+
resolution: "caniuse-lite@npm:1.0.30001390"
2591+
checksum: 5ba4ae64e27c61e1c7d7125223159d6cf7fa3cdbf8f00b9ec83a06f274ff45ddcbfebe509716fa31ae2664b70ef9e1d1c4a5b9430e717852992358121d9ee9be
25922592
languageName: node
25932593
linkType: hard
25942594

0 commit comments

Comments
 (0)