Skip to content

Commit 8947a9f

Browse files
committed
test: split up listFiles to be testable w/o stubs
1 parent bf820e4 commit 8947a9f

File tree

2 files changed

+81
-77
lines changed

2 files changed

+81
-77
lines changed

packages/core/src/shared/clients/s3.ts

Lines changed: 69 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ import { defaultPartition } from '../regions/regionProvider'
1717
import { AsyncCollection, toCollection } from '../utilities/asyncCollection'
1818
import { toStream } from '../utilities/collectionUtils'
1919
import {
20+
_Object,
2021
BucketLocationConstraint,
2122
CreateBucketCommand,
2223
DeleteBucketCommand,
2324
GetObjectCommand,
2425
GetObjectCommandInput,
2526
GetObjectCommandOutput,
27+
HeadObjectCommand,
28+
HeadObjectOutput,
29+
ListObjectsV2Command,
30+
ListObjectsV2Output,
2631
PutObjectCommand,
2732
S3Client as S3ClientSDK,
2833
} from '@aws-sdk/client-s3'
@@ -274,7 +279,7 @@ export class S3Client extends ClientWrapper<S3ClientSDK> {
274279
*/
275280
public async downloadFileStream(bucketName: string, key: string): Promise<Readable> {
276281
// GetObject response body is now a `StreamingBlobPayloadOutputTypes` from @smithy/types.
277-
// this is a general type for web/node streams, therefore we must cast the nodes streaming type.
282+
// this is a general type for web/node streams, therefore we must cast to nodes streaming type.
278283
const response = await this.makeRequest<GetObjectCommandInput, GetObjectCommandOutput, GetObjectCommand>(
279284
GetObjectCommand,
280285
{
@@ -290,10 +295,9 @@ export class S3Client extends ClientWrapper<S3ClientSDK> {
290295
return (response.Body as Readable) ?? new Readable()
291296
}
292297

293-
public async headObject(request: HeadObjectRequest): Promise<S3.HeadObjectOutput> {
294-
const s3 = await this.createS3()
298+
public async headObject(request: HeadObjectRequest): Promise<HeadObjectOutput> {
295299
getLogger().debug('HeadObject called with request: %O', request)
296-
return s3.headObject({ Bucket: request.bucketName, Key: request.key }).promise()
300+
return this.makeRequest(HeadObjectCommand, { Bucket: request.bucketName, Key: request.key })
297301
}
298302

299303
/**
@@ -441,6 +445,63 @@ export class S3Client extends ClientWrapper<S3ClientSDK> {
441445
return { buckets: bucketsInRegion }
442446
}
443447

448+
private async listObjectsV2(request: ListFilesRequest): Promise<ListObjectsV2Output> {
449+
return await this.makeRequest(ListObjectsV2Command, {
450+
Bucket: request.bucketName,
451+
Delimiter: DEFAULT_DELIMITER,
452+
MaxKeys: request.maxResults ?? DEFAULT_MAX_KEYS,
453+
/**
454+
* Set '' as the default prefix to ensure that the bucket's content will be displayed
455+
* when the user has at least list access to the root of the bucket
456+
* https://github.com/aws/aws-toolkit-vscode/issues/4643
457+
* @default ''
458+
*/
459+
Prefix: request.folderPath ?? defaultPrefix,
460+
ContinuationToken: request.continuationToken,
461+
})
462+
}
463+
464+
private extractFilesFromResponse(
465+
listObjectsRsp: ListObjectsV2Output,
466+
bucketName: string,
467+
folderPath: string | undefined
468+
): File[] {
469+
const bucket = new DefaultBucket({
470+
partitionId: this.partitionId,
471+
region: this.regionCode,
472+
name: bucketName,
473+
})
474+
return _(listObjectsRsp.Contents)
475+
.reject((file) => file.Key === folderPath)
476+
.map((file) => {
477+
assertHasProps(file, 'Key')
478+
return toFile(bucket, file)
479+
})
480+
.value()
481+
}
482+
483+
private extractFoldersFromResponse(listObjectsRsp: ListObjectsV2Output, bucketName: string): Folder[] {
484+
return _(listObjectsRsp.CommonPrefixes)
485+
.map((prefix) => prefix.Prefix)
486+
.compact()
487+
.map((path) => new DefaultFolder({ path, partitionId: this.partitionId, bucketName }))
488+
.value()
489+
}
490+
491+
public listFilesFromResponse(
492+
listObjectsRsp: ListObjectsV2Output,
493+
bucketName: string,
494+
folderPath: string | undefined
495+
) {
496+
const files = this.extractFilesFromResponse(listObjectsRsp, bucketName, folderPath)
497+
const folders = this.extractFoldersFromResponse(listObjectsRsp, bucketName)
498+
return {
499+
files,
500+
folders,
501+
continuationToken: listObjectsRsp.NextContinuationToken,
502+
}
503+
}
504+
444505
/**
445506
* Lists files and folders in a folder or inside the bucket root.
446507
*
@@ -463,48 +524,9 @@ export class S3Client extends ClientWrapper<S3ClientSDK> {
463524
*/
464525
public async listFiles(request: ListFilesRequest): Promise<ListFilesResponse> {
465526
getLogger().debug('ListFiles called with request: %O', request)
527+
const output = await this.listObjectsV2(request)
528+
const response = this.listFilesFromResponse(output, request.bucketName, request.folderPath)
466529

467-
const s3 = await this.createS3()
468-
const bucket = new DefaultBucket({
469-
partitionId: this.partitionId,
470-
region: this.regionCode,
471-
name: request.bucketName,
472-
})
473-
const output = await s3
474-
.listObjectsV2({
475-
Bucket: bucket.name,
476-
Delimiter: DEFAULT_DELIMITER,
477-
MaxKeys: request.maxResults ?? DEFAULT_MAX_KEYS,
478-
/**
479-
* Set '' as the default prefix to ensure that the bucket's content will be displayed
480-
* when the user has at least list access to the root of the bucket
481-
* https://github.com/aws/aws-toolkit-vscode/issues/4643
482-
* @default ''
483-
*/
484-
Prefix: request.folderPath ?? defaultPrefix,
485-
ContinuationToken: request.continuationToken,
486-
})
487-
.promise()
488-
489-
const files: File[] = _(output.Contents)
490-
.reject((file) => file.Key === request.folderPath)
491-
.map((file) => {
492-
assertHasProps(file, 'Key')
493-
return toFile(bucket, file)
494-
})
495-
.value()
496-
497-
const folders: Folder[] = _(output.CommonPrefixes)
498-
.map((prefix) => prefix.Prefix)
499-
.compact()
500-
.map((path) => new DefaultFolder({ path, partitionId: this.partitionId, bucketName: request.bucketName }))
501-
.value()
502-
503-
const response: ListFilesResponse = {
504-
files,
505-
folders,
506-
continuationToken: output.NextContinuationToken,
507-
}
508530
getLogger().debug('ListFiles returned response: %O', response)
509531
return response
510532
}
@@ -717,7 +739,7 @@ export class DefaultFolder {
717739
}
718740
}
719741

720-
export interface File extends S3.Object, S3.HeadObjectOutput {
742+
export interface File extends _Object, HeadObjectOutput {
721743
readonly name: string
722744
readonly key: string
723745
readonly arn: string
@@ -726,7 +748,7 @@ export interface File extends S3.Object, S3.HeadObjectOutput {
726748
readonly eTag?: string
727749
}
728750

729-
export function toFile(bucket: Bucket, resp: RequiredProps<S3.Object, 'Key'>, delimiter = DEFAULT_DELIMITER): File {
751+
export function toFile(bucket: Bucket, resp: RequiredProps<_Object, 'Key'>, delimiter = DEFAULT_DELIMITER): File {
730752
return {
731753
key: resp.Key,
732754
arn: `${bucket.arn}/${resp.Key}`,

packages/core/src/test/shared/clients/defaultS3Client.test.ts

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import { ListObjectVersionsOutput, ListObjectVersionsRequest } from 'aws-sdk/cli
99
import { FileStreams } from '../../../shared/utilities/streamUtilities'
1010
import * as vscode from 'vscode'
1111
import { DefaultBucket, DefaultFolder, S3Client, toFile } from '../../../shared/clients/s3'
12-
import { DEFAULT_DELIMITER, DEFAULT_MAX_KEYS } from '../../../shared/clients/s3'
12+
import { DEFAULT_MAX_KEYS } from '../../../shared/clients/s3'
1313
import { FakeFileStreams } from './fakeFileStreams'
1414
import globals from '../../../shared/extensionGlobals'
1515
import sinon from 'sinon'
1616
import { Stub, stub } from '../../utilities/stubber'
17+
import { ListObjectsV2Output } from '@aws-sdk/client-s3'
1718

1819
class FakeProgressCaptor {
1920
public progress = 0
@@ -48,9 +49,7 @@ describe('DefaultS3Client', function () {
4849
const fileLastModified = new Date(2020, 5, 4)
4950
const fileData = 'fileData'
5051
const fileLocation = vscode.Uri.file('/file.jpg')
51-
const continuationToken = 'continuationToken'
5252
const nextContinuationToken = 'nextContinuationToken'
53-
const maxResults = 20
5453
const nextKeyMarker = 'nextKeyMarker'
5554
const nextVersionIdMarker = 'nextVersionIdMarker'
5655
const error: AWSError = new FakeAwsError('Expected failure') as AWSError
@@ -307,43 +306,26 @@ describe('DefaultS3Client', function () {
307306
})
308307
})
309308

310-
describe('listFiles', function () {
311-
it('lists files and folders', async function () {
309+
describe('listFilesFromResponse', function () {
310+
it('parses response for list of files and folders', async function () {
312311
const folder = { Key: folderPath, Size: fileSizeBytes, LastModified: fileLastModified }
313312
const file = { Key: fileKey, Size: fileSizeBytes, LastModified: fileLastModified }
314-
const listStub = sinon.stub().returns(
315-
success({
316-
Contents: [folder, file],
317-
CommonPrefixes: [{ Prefix: subFolderPath }, { Prefix: emptySubFolderPath }],
318-
NextContinuationToken: nextContinuationToken,
319-
})
320-
)
321-
mockS3.listObjectsV2 = listStub
313+
const sdkResponse: ListObjectsV2Output = {
314+
Contents: [folder, file],
315+
CommonPrefixes: [{ Prefix: subFolderPath }, { Prefix: emptySubFolderPath }],
316+
NextContinuationToken: nextContinuationToken,
317+
}
322318

323-
const response = await createClient().listFiles({ bucketName, folderPath, continuationToken, maxResults })
324-
assert.deepStrictEqual(response, {
319+
const processedResponse = createClient().listFilesFromResponse(sdkResponse, bucketName, folderPath)
320+
321+
assert.deepStrictEqual(processedResponse, {
325322
files: [toFile(bucket, file)],
326323
folders: [
327324
new DefaultFolder({ partitionId: partition, bucketName, path: subFolderPath }),
328325
new DefaultFolder({ partitionId: partition, bucketName, path: emptySubFolderPath }),
329326
],
330327
continuationToken: nextContinuationToken,
331328
})
332-
assert(
333-
listStub.calledOnceWith({
334-
Bucket: bucketName,
335-
Delimiter: DEFAULT_DELIMITER,
336-
MaxKeys: maxResults,
337-
Prefix: folderPath,
338-
ContinuationToken: continuationToken,
339-
})
340-
)
341-
})
342-
343-
it('throws an Error on listFiles failure', async function () {
344-
mockS3.listObjectsV2 = sinon.stub().returns(failure())
345-
346-
await assert.rejects(createClient().listFiles({ bucketName, folderPath, continuationToken }), error)
347329
})
348330
})
349331

0 commit comments

Comments
 (0)