Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/gateways/api-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,84 @@ export const ApiGateway = {
);
},

/**
* Prefer server-assembled bundle delivery. The API returns a signed manifest
* plus a URL to a delivery service that streams the ZIP straight from
* storage, so large downloads don't flow through the API itself. The client
* just relays the signed `{ manifest, sig }` to that URL — no auth header,
* because the manifest is already signed.
*
* Returns true once the bundle has been streamed to disk. Returns false when
* the bundle path is unavailable or anything about it doesn't pan out — a
* `501` (deployment doesn't offer it), a non-OK manifest response, a body
* that isn't a manifest, or a delivery-service error — so the caller can fall
* back to the inline download endpoint. Definitive errors (e.g. not found)
* surface through that inline path instead.
*/
async tryBundleDownload(
baseUrl: string,
auth: AuthContext,
manifestEndpoint: string,
destinationPath: string,
operation: string,
): Promise<boolean> {
let manifestRes: Response;
try {
manifestRes = await fetch(`${baseUrl}${manifestEndpoint}`, {
headers: { ...auth.headers },
method: 'GET',
});
} catch {
return false;
}

// 501 => this deployment has no bundle delivery; any other non-OK => let
// the inline path re-request and surface the real error.
if (!manifestRes.ok) {
return false;
}

let bundle: {
bundleUrl?: string;
manifest?: string;
sig?: string;
};
try {
bundle = (await manifestRes.json()) as typeof bundle;
} catch {
return false;
}
if (!bundle?.bundleUrl || !bundle?.manifest || !bundle?.sig) {
return false;
}

let zipRes: Response;
try {
zipRes = await fetch(bundle.bundleUrl, {
body: JSON.stringify({ manifest: bundle.manifest, sig: bundle.sig }),
// No auth header: the manifest is signed and is the access token.
headers: { 'content-type': 'application/json' },
method: 'POST',
});
} catch {
return false;
}
if (!zipRes.ok) {
return false;
}

// A mid-stream failure (dropped/truncated connection) or a null body on an
// otherwise-OK response throws here — fall back to the inline path rather
// than let it escape past the caller's fallback. The inline path re-opens
// the destination with flags: 'w', truncating any partial file left behind.
try {
await this.streamResponseToFile(zipRes, destinationPath, operation);
} catch {
return false;
}
return true;
},

async checkForExistingUpload(
baseUrl: string,
auth: AuthContext,
Expand Down Expand Up @@ -219,6 +297,19 @@ export const ApiGateway = {
results: 'ALL' | 'FAILED',
artifactsPath: string = './artifacts.zip',
) {
// Prefer bundle delivery; fall back to the inline download below.
if (
await this.tryBundleDownload(
baseUrl,
auth,
`/results/${uploadId}/artifacts-bundle?results=${results}`,
artifactsPath,
'Failed to download artifacts',
)
) {
return;
}

try {
const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
body: JSON.stringify({ results }),
Expand Down Expand Up @@ -677,6 +768,22 @@ export const ApiGateway = {
const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename);
const url = `${baseUrl}${endpoint}`;

// The HTML report is a ZIP bundle; prefer bundle delivery when available.
// (junit is a single small file and allure has its own endpoint — both stay
// on the inline path.)
if (
reportType === 'html' &&
(await this.tryBundleDownload(
baseUrl,
auth,
`/results/${uploadId}/report-bundle`,
finalReportPath,
errorPrefix,
))
) {
return;
}

try {
// Make the download request
const res = await fetch(url, {
Expand Down
153 changes: 153 additions & 0 deletions test/unit/report-download.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,157 @@ describe('ReportDownloadService', () => {
expect(warnings.join(' ')).to.match(/failed to download allure/i);
});
});

// -------------------------------------------------------------------------
// bundle delivery
// -------------------------------------------------------------------------

describe('bundle delivery', () => {
const BASE = {
auth: TEST_AUTH,
apiUrl: 'https://api.example.com',
uploadId: 'run-42',
};

let calls: Array<{
headers: Record<string, string>;
method: string;
url: string;
}>;

/**
* Route fetch by URL: a `*-bundle` endpoint returns a signed manifest, and
* the manifest's `bundleUrl` streams the ZIP. Records every call so the
* flow (manifest GET, then bundle POST, no inline call) can be asserted.
*/
function mockBundleFetch(
opts: { manifestStatus?: number; zipBody?: string } = {},
) {
const { manifestStatus = 200, zipBody = 'bundle-zip' } = opts;
const encoder = new TextEncoder();
const stream = (s: string) =>
new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(s));
controller.close();
},
});

const impl = async (
input: URL | string,
init?: RequestInit,
): Promise<Response> => {
const url = input.toString();
calls.push({
headers: Object.fromEntries(
Object.entries((init?.headers as Record<string, string>) ?? {}),
),
method: (init?.method ?? 'GET').toUpperCase(),
url,
});

if (url.includes('artifacts-bundle') || url.includes('report-bundle')) {
return new Response(
stream(
JSON.stringify({
bundleUrl: 'https://cdn.example.com/bundle',
entryCount: 3,
filename: 'artifacts-all.zip',
manifest: '{"version":1}',
sig: 'SIG',
}),
),
{
headers: { 'content-type': 'application/json' },
status: manifestStatus,
},
);
}
if (url === 'https://cdn.example.com/bundle') {
return new Response(stream(zipBody), { status: 200 });
}
return new Response(stream('inline-zip'), { status: 200 });
};
globalThis.fetch = impl as typeof fetch;
}

beforeEach(() => {
calls = [];
});

it('streams from the bundle URL and skips the inline endpoint', async () => {
mockBundleFetch();
const outPath = path.join(tempDir, 'bundle.zip');

await service.downloadArtifacts({
...BASE,
artifactsPath: outPath,
downloadType: 'ALL',
});

expect(calls[0]).to.include({
method: 'GET',
url: 'https://api.example.com/results/run-42/artifacts-bundle?results=ALL',
});
expect(calls[1]).to.include({
method: 'POST',
url: 'https://cdn.example.com/bundle',
});
expect(calls.some((c) => c.url.endsWith('/download'))).to.be.false;
expect(fs.readFileSync(outPath, 'utf8')).to.equal('bundle-zip');
});

it('does not send the auth header to the (pre-signed) bundle URL', async () => {
mockBundleFetch();

await service.downloadArtifacts({
...BASE,
artifactsPath: path.join(tempDir, 'bundle-noauth.zip'),
downloadType: 'ALL',
});

const bundlePost = calls.find(
(c) => c.url === 'https://cdn.example.com/bundle',
);
expect(bundlePost).to.not.be.undefined;
expect(bundlePost!.headers['x-app-api-key']).to.be.undefined;
});

it('falls back to the inline download when unavailable (501)', async () => {
mockBundleFetch({ manifestStatus: 501 });
const outPath = path.join(tempDir, 'bundle-fallback.zip');

await service.downloadArtifacts({
...BASE,
artifactsPath: outPath,
downloadType: 'ALL',
});

expect(
calls.some((c) => c.url.endsWith('/artifacts-bundle?results=ALL')),
).to.be.true;
expect(
calls.some((c) => c.method === 'POST' && c.url.endsWith('/download')),
).to.be.true;
expect(fs.readFileSync(outPath, 'utf8')).to.equal('inline-zip');
});

it('uses bundle delivery for the html report', async () => {
mockBundleFetch();

await service.downloadReports({
...BASE,
htmlPath: path.join(tempDir, 'report-bundle.zip'),
reportType: 'html',
});

expect(calls[0].url).to.equal(
'https://api.example.com/results/run-42/report-bundle',
);
expect(calls[1]).to.include({
method: 'POST',
url: 'https://cdn.example.com/bundle',
});
});
});
});
Loading