Path: blob/main/extensions/copilot/src/shared-fetch-utils/common/test/httpResponse.spec.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { describe, expect, it } from 'vitest';6import type { HttpHeaders } from '../fetchTypes';7import { cloneResponse, type CloneableResponse } from '../httpResponse';89// ── Helpers ─────────────────────────────────────────────────────────────1011function makeHeaders(entries: Record<string, string> = {}): HttpHeaders {12const map = new Map(Object.entries(entries));13return { get: (name: string) => map.get(name.toLowerCase()) ?? null };14}1516function textStream(text: string): ReadableStream<Uint8Array> {17return new ReadableStream({18start(controller) {19controller.enqueue(new TextEncoder().encode(text));20controller.close();21},22});23}2425function multiChunkStream(chunks: string[]): ReadableStream<Uint8Array> {26return new ReadableStream({27start(controller) {28for (const chunk of chunks) {29controller.enqueue(new TextEncoder().encode(chunk));30}31controller.close();32},33});34}3536/** Simulates the platform `DestroyableStream` which exposes `toReadableStream()`. */37function fakeDestroyableStream(text: string) {38return { toReadableStream: () => textStream(text) };39}4041function makeResponse(status: number, headers: HttpHeaders, body: ReadableStream<Uint8Array> | null): CloneableResponse {42return { status, headers, body };43}4445// ── cloneResponse ───────────────────────────────────────────────────────4647describe('cloneResponse', () => {4849describe('null body', () => {50it('returns two independent responses with null body', () => {51const headers = makeHeaders({ 'x-test': '1' });52const [a, b] = cloneResponse(makeResponse(200, headers, null));5354expect(a.status).toBe(200);55expect(b.status).toBe(200);56expect(a.body).toBeNull();57expect(b.body).toBeNull();58expect(a).not.toBe(b);59});6061it('text() returns empty string for null body', async () => {62const [a, b] = cloneResponse(makeResponse(204, makeHeaders(), null));63expect(await a.text()).toBe('');64expect(await b.text()).toBe('');65});66});6768describe('ReadableStream body', () => {69it('produces two independently consumable streams', async () => {70const response = makeResponse(200, makeHeaders(), textStream('hello'));71const [a, b] = cloneResponse(response);7273expect(await a.text()).toBe('hello');74expect(await b.text()).toBe('hello');75});7677it('text() handles multi-chunk streams', async () => {78const response = makeResponse(200, makeHeaders(), multiChunkStream(['hel', 'lo ', 'world']));79const [a, b] = cloneResponse(response);8081expect(await a.text()).toBe('hello world');82expect(await b.text()).toBe('hello world');83});8485it('json() parses body as JSON', async () => {86const payload = { key: 'value', n: 42 };87const response = makeResponse(200, makeHeaders(), textStream(JSON.stringify(payload)));88const [a, b] = cloneResponse(response);8990expect(await a.json()).toEqual(payload);91expect(await b.json()).toEqual(payload);92});9394it('preserves status and headers on both clones', () => {95const headers = makeHeaders({ 'content-type': 'application/json', 'etag': '"v1"' });96const response = makeResponse(200, headers, textStream('{}'));97const [a, b] = cloneResponse(response);9899expect(a.status).toBe(200);100expect(b.status).toBe(200);101expect(a.headers.get('content-type')).toBe('application/json');102expect(b.headers.get('etag')).toBe('"v1"');103});104});105106describe('DestroyableStream body (toReadableStream)', () => {107it('unwraps DestroyableStream and produces two consumable clones', async () => {108const response: CloneableResponse = {109status: 200,110headers: makeHeaders(),111body: fakeDestroyableStream('from destroyable'),112};113const [a, b] = cloneResponse(response);114115expect(await a.text()).toBe('from destroyable');116expect(await b.text()).toBe('from destroyable');117});118});119120describe('repeated cloning', () => {121it('can clone a clone', async () => {122const response = makeResponse(200, makeHeaders(), textStream('original'));123const [first, second] = cloneResponse(response);124125// Clone one of the halves again126const [firstA, firstB] = cloneResponse(first);127128expect(await firstA.text()).toBe('original');129expect(await firstB.text()).toBe('original');130expect(await second.text()).toBe('original');131});132});133});134135136