Files
floci-ui/packages/api/src/adapter-aws/AwsStorageAdapter.test.ts
T

229 lines
10 KiB
TypeScript

import {describe, expect, test} from 'bun:test'
import {
GetBucketNotificationConfigurationCommand,
GetBucketLocationCommand,
GetBucketTaggingCommand,
GetBucketWebsiteCommand,
ListBucketsCommand,
PutBucketNotificationConfigurationCommand,
PutBucketWebsiteCommand,
type S3Client,
} from '@aws-sdk/client-s3'
import {AddPermissionCommand, GetFunctionCommand, RemovePermissionCommand, type LambdaClient} from '@aws-sdk/client-lambda'
import {AwsStorageAdapter} from './AwsStorageAdapter'
function fakeS3(overrides: Partial<{
buckets: Array<{Name: string; CreationDate?: Date}>
tagSet: Array<{Key: string; Value: string}>
taggingError: Error
onGetBucketTagging: (bucketName: string) => void
}> = {}): S3Client {
return {
send: async (command: unknown) => {
if (command instanceof ListBucketsCommand) {
return {
Buckets: overrides.buckets ?? [
{Name: 'my-tf-test-bucket', CreationDate: new Date('2026-01-01')},
],
}
}
if (command instanceof GetBucketTaggingCommand) {
overrides.onGetBucketTagging?.((command as {input: {Bucket: string}}).input.Bucket)
if (overrides.taggingError) throw overrides.taggingError
return {TagSet: overrides.tagSet ?? [{Key: 'Environment', Value: 'dev'}]}
}
if (command instanceof GetBucketLocationCommand) return {LocationConstraint: 'us-west-2'}
throw new Error(`Unexpected command: ${command?.constructor?.name}`)
},
} as unknown as S3Client
}
describe('AwsStorageAdapter', () => {
test('list includes bucket tags in metadata', async () => {
const adapter = new AwsStorageAdapter(fakeS3())
const result = await adapter.list()
expect(result).toHaveLength(1)
expect(result[0].metadata.tags).toEqual([{key: 'Environment', value: 'dev'}])
expect(result[0].metadata.tagsUnavailable).toBe(false)
})
test('list returns empty tags for untagged bucket instead of throwing', async () => {
const error = new Error('The TagSet does not exist')
error.name = 'NoSuchTagSet'
const adapter = new AwsStorageAdapter(fakeS3({taggingError: error}))
const result = await adapter.list()
expect(result[0].metadata.tags).toEqual([])
expect(result[0].metadata.tagsUnavailable).toBe(false)
})
test('list does not fail entirely when tag fetch is denied for a bucket', async () => {
const error = new Error('Access Denied')
error.name = 'AccessDenied'
const adapter = new AwsStorageAdapter(fakeS3({taggingError: error}))
const result = await adapter.list()
expect(result).toHaveLength(1)
expect(result[0].metadata.tags).toEqual([])
})
test('list marks tags unavailable (not empty) when tag fetch is denied', async () => {
const error = new Error('Access Denied')
error.name = 'AccessDenied'
const adapter = new AwsStorageAdapter(fakeS3({taggingError: error}))
const result = await adapter.list()
expect(result[0].metadata.tagsUnavailable).toBe(true)
})
test('list marks tags unavailable on transport/runtime failures too', async () => {
const error = new Error('socket hang up')
error.name = 'TimeoutError'
const adapter = new AwsStorageAdapter(fakeS3({taggingError: error}))
const result = await adapter.list()
expect(result[0].metadata.tags).toEqual([])
expect(result[0].metadata.tagsUnavailable).toBe(true)
})
test('search filters buckets before fetching tags, not after', async () => {
const requestedBuckets: string[] = []
const adapter = new AwsStorageAdapter(fakeS3({
buckets: [
{Name: 'prod-logs'},
{Name: 'dev-logs'},
{Name: 'prod-assets'},
],
onGetBucketTagging: (bucketName) => requestedBuckets.push(bucketName),
}))
const result = await adapter.list({search: 'prod'})
expect(result.map((r) => r.name).sort()).toEqual(['prod-assets', 'prod-logs'])
expect(requestedBuckets.sort()).toEqual(['prod-assets', 'prod-logs'])
})
test('gets normalized website configuration and maps no website configuration to null', async () => {
const s3 = {
send: async (command: unknown) => {
if (command instanceof GetBucketWebsiteCommand) {
return {RedirectAllRequestsTo: {HostName: 'www.example.com', Protocol: 'https'}}
}
throw new Error(`Unexpected command: ${command?.constructor?.name}`)
},
} as unknown as S3Client
const adapter = new AwsStorageAdapter(s3)
expect(await adapter.getS3BucketWebsite('website-bucket')).toEqual({mode: 'redirect', hostName: 'www.example.com', protocol: 'https'})
const absent = new Error('No website configuration')
absent.name = 'NoSuchWebsiteConfiguration'
const missing = new AwsStorageAdapter({send: async () => { throw absent }} as unknown as S3Client)
expect(await missing.getS3BucketWebsite('website-bucket')).toBeNull()
})
test('puts mutually exclusive S3 website configuration shapes', async () => {
const sent: unknown[] = []
const s3 = {send: async (command: unknown) => { sent.push(command); return {} }} as unknown as S3Client
const adapter = new AwsStorageAdapter(s3)
await adapter.putS3BucketWebsite('website-bucket', {mode: 'index', indexDocument: 'index.html', errorDocument: 'error.html'})
await adapter.putS3BucketWebsite('website-bucket', {mode: 'redirect', hostName: 'www.example.com', protocol: 'https'})
const [index, redirect] = sent as PutBucketWebsiteCommand[]
expect(index.input.WebsiteConfiguration).toEqual({IndexDocument: {Suffix: 'index.html'}, ErrorDocument: {Key: 'error.html'}})
expect(redirect.input.WebsiteConfiguration).toEqual({RedirectAllRequestsTo: {HostName: 'www.example.com', Protocol: 'https'}})
})
test('adds an S3 Lambda trigger without replacing other bucket notifications', async () => {
const sent: unknown[] = []
const s3 = {
send: async (command: unknown) => {
sent.push(command)
if (command instanceof GetBucketNotificationConfigurationCommand) {
return {
QueueConfigurations: [{Id: 'queue-trigger', QueueArn: 'arn:aws:sqs:us-east-1:000000000000:jobs', Events: ['s3:ObjectCreated:*']}],
EventBridgeConfiguration: {},
}
}
if (command instanceof PutBucketNotificationConfigurationCommand) return {}
throw new Error(`Unexpected command: ${command?.constructor?.name}`)
},
} as unknown as S3Client
const lambda = fakeLambda()
const adapter = new AwsStorageAdapter(s3, lambda.client, '000000000000')
await adapter.createS3LambdaTrigger('uploads', {
functionName: 'thumbnailer',
events: ['s3:ObjectCreated:*'],
prefix: 'images/',
suffix: '.jpg',
})
const put = sent.find((command) => command instanceof PutBucketNotificationConfigurationCommand) as PutBucketNotificationConfigurationCommand
expect(put.input.NotificationConfiguration?.QueueConfigurations).toHaveLength(1)
expect(put.input.NotificationConfiguration?.EventBridgeConfiguration).toEqual({})
expect(put.input.NotificationConfiguration?.LambdaFunctionConfigurations).toEqual([{
Id: 'floci-ui-s3-thumbnailer-s3-ObjectCreated---images---jpg',
LambdaFunctionArn: 'arn:aws:lambda:us-east-1:000000000000:function:thumbnailer',
Events: ['s3:ObjectCreated:*'],
Filter: {Key: {FilterRules: [{Name: 'prefix', Value: 'images/'}, {Name: 'suffix', Value: '.jpg'}]}},
}])
const permission = lambda.sent.find((command) => command instanceof AddPermissionCommand) as AddPermissionCommand
expect(permission.input).toMatchObject({
FunctionName: 'thumbnailer',
Principal: 's3.amazonaws.com',
SourceArn: 'arn:aws:s3:::uploads',
SourceAccount: '000000000000',
})
})
test('removes the S3 permission only after the final bucket trigger for a Lambda is deleted', async () => {
const sent: unknown[] = []
const s3 = {
send: async (command: unknown) => {
sent.push(command)
if (command instanceof GetBucketNotificationConfigurationCommand) {
return {LambdaFunctionConfigurations: [{
Id: 'trigger-a',
LambdaFunctionArn: 'arn:aws:lambda:us-east-1:000000000000:function:thumbnailer',
Events: ['s3:ObjectCreated:*'],
}]}
}
if (command instanceof PutBucketNotificationConfigurationCommand) return {}
throw new Error(`Unexpected command: ${command?.constructor?.name}`)
},
} as unknown as S3Client
const lambda = fakeLambda()
const adapter = new AwsStorageAdapter(s3, lambda.client)
await adapter.deleteS3LambdaTrigger('uploads', 'trigger-a')
const put = sent.find((command) => command instanceof PutBucketNotificationConfigurationCommand) as PutBucketNotificationConfigurationCommand
expect(put.input.NotificationConfiguration?.LambdaFunctionConfigurations).toEqual([])
expect(lambda.sent.some((command) => command instanceof RemovePermissionCommand)).toBe(true)
})
})
function fakeLambda(): {client: LambdaClient; sent: unknown[]} {
const sent: unknown[] = []
return {
sent,
client: {
send: async (command: unknown) => {
sent.push(command)
if (command instanceof GetFunctionCommand) {
return {Configuration: {FunctionArn: 'arn:aws:lambda:us-east-1:000000000000:function:thumbnailer'}}
}
if (command instanceof AddPermissionCommand || command instanceof RemovePermissionCommand) return {}
throw new Error(`Unexpected command: ${command?.constructor?.name}`)
},
} as unknown as LambdaClient,
}
}