Skip to content

Commit 98e0422

Browse files
Copiloticlanton
andauthored
Add unit tests for streaming cache APIs and fill buffer-based test gaps
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/86050f65-dd6c-45f4-ac41-95fdb860c053 Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
1 parent 1add881 commit 98e0422

3 files changed

Lines changed: 512 additions & 0 deletions

File tree

rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3Client.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,4 +634,187 @@ describe(AmazonS3Client.name, () => {
634634
);
635635
});
636636
});
637+
638+
describe('Streaming requests', () => {
639+
let realDate: typeof Date;
640+
let realSetTimeout: typeof setTimeout;
641+
beforeEach(() => {
642+
// mock date
643+
realDate = global.Date;
644+
global.Date = MockedDate as typeof Date;
645+
646+
// mock setTimeout
647+
realSetTimeout = global.setTimeout;
648+
global.setTimeout = ((callback: () => void, time: number) => {
649+
return realSetTimeout(callback, 1);
650+
}).bind(global) as typeof global.setTimeout;
651+
});
652+
653+
afterEach(() => {
654+
jest.restoreAllMocks();
655+
global.Date = realDate;
656+
global.setTimeout = realSetTimeout.bind(global);
657+
});
658+
659+
describe('Getting an object stream', () => {
660+
async function makeStreamGetRequestAsync(
661+
credentials: IAmazonS3Credentials | undefined,
662+
options: IAmazonS3BuildCacheProviderOptionsAdvanced,
663+
objectName: string,
664+
status: number,
665+
statusText?: string
666+
): Promise<{ result: NodeJS.ReadableStream | undefined; spy: jest.SpyInstance }> {
667+
const { Readable } = await import('node:stream');
668+
const mockStream = new Readable({ read() {} });
669+
670+
const spy: jest.SpyInstance = jest
671+
.spyOn(WebClient.prototype, 'fetchStreamAsync')
672+
.mockReturnValue(
673+
Promise.resolve({
674+
stream: mockStream,
675+
headers: {},
676+
status,
677+
statusText,
678+
ok: status >= 200 && status < 300,
679+
redirected: false
680+
})
681+
);
682+
683+
const s3Client: AmazonS3Client = new AmazonS3Client(credentials, options, webClient, terminal);
684+
const result = await s3Client.getObjectStreamAsync(objectName);
685+
return { result, spy };
686+
}
687+
688+
it('Can get an object stream', async () => {
689+
const { result, spy } = await makeStreamGetRequestAsync(
690+
{
691+
accessKeyId: 'accessKeyId',
692+
secretAccessKey: 'secretAccessKey',
693+
sessionToken: undefined
694+
},
695+
DUMMY_OPTIONS,
696+
'abc123',
697+
200
698+
);
699+
expect(result).toBeDefined();
700+
expect(spy).toHaveBeenCalledTimes(1);
701+
expect(spy.mock.calls[0]).toMatchSnapshot();
702+
spy.mockRestore();
703+
});
704+
705+
it('Returns undefined for a 404 (missing) object stream', async () => {
706+
const { result, spy } = await makeStreamGetRequestAsync(
707+
{
708+
accessKeyId: 'accessKeyId',
709+
secretAccessKey: 'secretAccessKey',
710+
sessionToken: undefined
711+
},
712+
DUMMY_OPTIONS,
713+
'abc123',
714+
404,
715+
'Not Found'
716+
);
717+
expect(result).toBeUndefined();
718+
expect(spy).toHaveBeenCalledTimes(1);
719+
spy.mockRestore();
720+
});
721+
});
722+
723+
describe('Uploading an object stream', () => {
724+
it('Throws an error if credentials are not provided', async () => {
725+
const { Readable } = await import('node:stream');
726+
const s3Client: AmazonS3Client = new AmazonS3Client(
727+
undefined,
728+
{ s3Endpoint: 'http://foo.bar.baz', ...DUMMY_OPTIONS_WITHOUT_ENDPOINT },
729+
webClient,
730+
terminal
731+
);
732+
733+
const mockStream = new Readable({ read() {} });
734+
try {
735+
await s3Client.uploadObjectStreamAsync('temp', mockStream);
736+
fail('Expected an exception to be thrown');
737+
} catch (e) {
738+
expect(e).toMatchSnapshot();
739+
}
740+
});
741+
742+
it('Uploads a stream successfully', async () => {
743+
const { Readable } = await import('node:stream');
744+
const mockStream = new Readable({ read() {} });
745+
const responseStream = new Readable({ read() {} });
746+
747+
const spy: jest.SpyInstance = jest
748+
.spyOn(WebClient.prototype, 'fetchStreamAsync')
749+
.mockReturnValue(
750+
Promise.resolve({
751+
stream: responseStream,
752+
headers: {},
753+
status: 200,
754+
statusText: 'OK',
755+
ok: true,
756+
redirected: false
757+
})
758+
);
759+
760+
const s3Client: AmazonS3Client = new AmazonS3Client(
761+
{
762+
accessKeyId: 'accessKeyId',
763+
secretAccessKey: 'secretAccessKey',
764+
sessionToken: undefined
765+
},
766+
DUMMY_OPTIONS,
767+
webClient,
768+
terminal
769+
);
770+
771+
await s3Client.uploadObjectStreamAsync('abc123', mockStream);
772+
773+
expect(spy).toHaveBeenCalledTimes(1);
774+
expect(spy.mock.calls[0]).toMatchSnapshot();
775+
spy.mockRestore();
776+
});
777+
778+
it('Does not retry on failure (stream consumed)', async () => {
779+
const { Readable } = await import('node:stream');
780+
const mockStream = new Readable({ read() {} });
781+
const responseStream = new Readable({ read() {} });
782+
783+
const spy: jest.SpyInstance = jest
784+
.spyOn(WebClient.prototype, 'fetchStreamAsync')
785+
.mockReturnValue(
786+
Promise.resolve({
787+
stream: responseStream,
788+
headers: {},
789+
status: 500,
790+
statusText: 'InternalServerError',
791+
ok: false,
792+
redirected: false
793+
})
794+
);
795+
796+
const s3Client: AmazonS3Client = new AmazonS3Client(
797+
{
798+
accessKeyId: 'accessKeyId',
799+
secretAccessKey: 'secretAccessKey',
800+
sessionToken: undefined
801+
},
802+
DUMMY_OPTIONS,
803+
webClient,
804+
terminal
805+
);
806+
807+
try {
808+
await s3Client.uploadObjectStreamAsync('abc123', mockStream);
809+
fail('Expected an exception to be thrown');
810+
} catch (e) {
811+
expect((e as Error).message).toContain('500');
812+
}
813+
814+
// Only 1 call - no retry for streams
815+
expect(spy).toHaveBeenCalledTimes(1);
816+
spy.mockRestore();
817+
});
818+
});
819+
});
637820
});

rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/__snapshots__/AmazonS3Client.test.ts.snap

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,55 @@ exports[`AmazonS3Client Rejects invalid S3 endpoint values 9`] = `"Invalid S3 en
548548
exports[`AmazonS3Client Rejects invalid S3 endpoint values 10`] = `"Invalid S3 endpoint. Some part of the hostname contains invalid characters or is too long"`;
549549

550550
exports[`AmazonS3Client Rejects invalid S3 endpoint values 11`] = `"Invalid S3 endpoint. Some part of the hostname contains invalid characters or is too long"`;
551+
552+
exports[`AmazonS3Client Streaming requests Getting an object stream Can get an object stream 1`] = `
553+
Array [
554+
"http://localhost:9000/abc123",
555+
Object {
556+
"headers": Object {
557+
"Authorization": "AWS4-HMAC-SHA256 Credential=accessKeyId/20200418/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=194608e9e7ba6d8aa4a019b3b6fd237e6b09ef1f45ff7fa60cbb81c1875538be",
558+
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
559+
"x-amz-date": "20200418T123242Z",
560+
},
561+
"verb": "GET",
562+
},
563+
]
564+
`;
565+
566+
exports[`AmazonS3Client Streaming requests Uploading an object stream Throws an error if credentials are not provided 1`] = `[Error: Credentials are required to upload objects to S3.]`;
567+
568+
exports[`AmazonS3Client Streaming requests Uploading an object stream Uploads a stream successfully 1`] = `
569+
Array [
570+
"http://localhost:9000/abc123",
571+
Object {
572+
"body": Readable {
573+
"_events": Object {
574+
"close": undefined,
575+
"data": undefined,
576+
"end": undefined,
577+
"error": undefined,
578+
"readable": undefined,
579+
},
580+
"_maxListeners": undefined,
581+
"_read": [Function],
582+
"_readableState": ReadableState {
583+
"awaitDrainWriters": null,
584+
"buffer": Array [],
585+
"bufferIndex": 0,
586+
"highWaterMark": 65536,
587+
"length": 0,
588+
"pipes": Array [],
589+
Symbol(kState): 1052940,
590+
},
591+
Symbol(shapeMode): true,
592+
Symbol(kCapture): false,
593+
},
594+
"headers": Object {
595+
"Authorization": "AWS4-HMAC-SHA256 Credential=accessKeyId/20200418/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=bfc5ee97ccfdb44b7f351c0b550a1ac9c2cdb661606dbba8a74c11be6b5b2b72",
596+
"x-amz-content-sha256": "UNSIGNED-PAYLOAD",
597+
"x-amz-date": "20200418T123242Z",
598+
},
599+
"verb": "PUT",
600+
},
601+
]
602+
`;

0 commit comments

Comments
 (0)