Skip to content
Merged
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
105 changes: 101 additions & 4 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/nestjs-s3-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
"devDependencies": {
"@nestjs/common": "^10.0.5",
"@nestjs/core": "^10.0.5",
"reflect-metadata": "^0.1.13",
"reflect-metadata": "0.2.2",
"rxjs": "^7.8.1"
},
"peerDependencies": {
"@nestjs/common": "^10",
"@nestjs/core": "^10",
"reflect-metadata": "^0.1",
"reflect-metadata": "^0.2",
"rxjs": "^7"
},
"publishConfig": {
Expand Down
3 changes: 3 additions & 0 deletions packages/nestjs-signed-url/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"prepack": "yarn run build",
"postpack": "rm -rf dist"
},
"dependencies": {
"@atls/nestjs-s3-client": "workspace:*"
Comment thread
TorinAsakura marked this conversation as resolved.
},
"devDependencies": {
"@atls/nestjs-gcs-client": "workspace:*",
"@google-cloud/storage": "7.14.0",
Expand Down
29 changes: 29 additions & 0 deletions packages/nestjs-signed-url/s3/tests/fixtures/client.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { S3Client } from '@atls/nestjs-s3-client'

import type { FakeS3Presigner } from './client.interfaces.js'

import { S3Client as AwsS3Client } from '@atls/nestjs-s3-client'

export const createFakeS3Client = (): S3Client =>
new AwsS3Client({
credentials: {
accessKeyId: 'access-key',
secretAccessKey: 'secret-key',
},
endpoint: 'https://s3.example.com',
region: 'auto',
})

export const createFakeS3Presigner = (signedUrl = 'signed-url'): FakeS3Presigner => {
const presigner: FakeS3Presigner = {
async sign(client, command, options): Promise<string> {
presigner.client = client
presigner.command = command
presigner.options = options

return signedUrl
},
}

return presigner
}
15 changes: 15 additions & 0 deletions packages/nestjs-signed-url/s3/tests/fixtures/client.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { GetObjectCommand } from '@atls/nestjs-s3-client'
import type { PutObjectCommand } from '@atls/nestjs-s3-client'
import type { S3Client } from '@atls/nestjs-s3-client'

import type { S3SignedUrlPresigner } from '../../../src/s3/interfaces.js'
import type { S3SignedUrlPresignerOptions } from '../../../src/s3/interfaces.js'

export type FakeS3Command = GetObjectCommand | PutObjectCommand

export interface FakeS3Presigner {
client?: S3Client
command?: FakeS3Command
options?: S3SignedUrlPresignerOptions
sign: S3SignedUrlPresigner
}
25 changes: 25 additions & 0 deletions packages/nestjs-signed-url/s3/tests/fixtures/module.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { S3Client } from '@atls/nestjs-s3-client'
import type { DynamicModule } from '@nestjs/common'

import type { TestingS3ClientFactory } from './module.interfaces.js'

class TestingS3ClientModule {}

export const TESTING_S3_CLIENT_FACTORY = Symbol('testing-s3-client-factory')

export const createTestingS3ClientModule = (client: S3Client): DynamicModule => {
const clientFactory: TestingS3ClientFactory = {
create: (): S3Client => client,
}

return {
module: TestingS3ClientModule,
providers: [
{
provide: TESTING_S3_CLIENT_FACTORY,
useValue: clientFactory,
},
],
exports: [TESTING_S3_CLIENT_FACTORY],
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { S3Client } from '@atls/nestjs-s3-client'

export interface TestingS3ClientFactory {
create: () => S3Client
}
141 changes: 141 additions & 0 deletions packages/nestjs-signed-url/s3/tests/gateway.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { FakeS3Presigner } from './fixtures/client.interfaces.js'

import assert from 'node:assert/strict'
import { beforeEach } from 'node:test'
import { describe } from 'node:test'
import { it } from 'node:test'
import { mock } from 'node:test'

import { GetObjectCommand } from '@atls/nestjs-s3-client'
import { PutObjectCommand } from '@atls/nestjs-s3-client'

import { S3SignedUrlGateway } from '../../src/s3/gateway.js'
import { createFakeS3Client } from './fixtures/client.fixture.js'
import { createFakeS3Presigner } from './fixtures/client.fixture.js'

describe('S3SignedUrlGateway', () => {
let gateway: S3SignedUrlGateway
let client: ReturnType<typeof createFakeS3Client>
let presigner: FakeS3Presigner

beforeEach(() => {
client = createFakeS3Client()
presigner = createFakeS3Presigner()
gateway = new S3SignedUrlGateway(client, presigner.sign)
})

describe('generateWriteUrl', () => {
it('maps S3 write options to PutObjectCommand and getSignedUrl', async () => {
const dateNowMock = mock.method(Date, 'now', () => 1000)

try {
const value = await gateway.generateWriteUrl('bucket', 'file.png', {
contentType: 'image/png',
expiresAt: 61000,
responseDisposition: 'inline',
s3: {
command: {
CacheControl: 'public, max-age=60',
Metadata: {
source: 'gateway-test',
},
},
presign: {
unhoistableHeaders: new Set(['x-amz-meta-source']),
},
},
})

assert.deepEqual(value, {
url: 'signed-url',
fields: [],
})
assert.equal(presigner.client, client)
assert.ok(presigner.command instanceof PutObjectCommand)
assert.deepEqual(presigner.command.input, {
CacheControl: 'public, max-age=60',
Metadata: {
source: 'gateway-test',
},
Bucket: 'bucket',
Key: 'file.png',
ContentType: 'image/png',
ContentDisposition: 'inline',
})
assert.deepEqual(presigner.options, {
expiresIn: 60,
signableHeaders: new Set(['content-type']),
unhoistableHeaders: new Set(['x-amz-meta-source']),
})
} finally {
dateNowMock.mock.restore()
}
})

it('signs write content type without requiring duplicated headers', async () => {
await gateway.generateWriteUrl('bucket', 'file.png', {
contentType: 'image/png',
})

assert.ok(presigner.command instanceof PutObjectCommand)
assert.deepEqual(presigner.command.input, {
Bucket: 'bucket',
Key: 'file.png',
ContentType: 'image/png',
})
assert.deepEqual(presigner.options, {
expiresIn: 900,
signableHeaders: new Set(['content-type']),
})
})
})

describe('generateReadUrl', () => {
it('maps provider-neutral read options to GetObjectCommand and getSignedUrl', async () => {
const value = await gateway.generateReadUrl('bucket', 'file.png', {
expiresInSeconds: 30,
responseDisposition: 'attachment; filename="file.png"',
s3: {
command: {
Range: 'bytes=0-1023',
VersionId: 'version-id',
},
presign: {
unsignableHeaders: new Set(['x-amz-checksum-mode']),
},
},
})

assert.deepEqual(value, {
url: 'signed-url',
fields: [],
})
assert.equal(presigner.client, client)
assert.ok(presigner.command instanceof GetObjectCommand)
assert.deepEqual(presigner.command.input, {
Range: 'bytes=0-1023',
VersionId: 'version-id',
Bucket: 'bucket',
Key: 'file.png',
ResponseContentDisposition: 'attachment; filename="file.png"',
})
assert.deepEqual(presigner.options, {
expiresIn: 30,
unsignableHeaders: new Set(['x-amz-checksum-mode']),
})
})

it('uses the package default expiration for read urls', async () => {
await gateway.generateReadUrl('bucket', 'file.png')

assert.ok(presigner.command instanceof GetObjectCommand)
assert.deepEqual(presigner.command.input, {
Bucket: 'bucket',
Key: 'file.png',
})
assert.deepEqual(presigner.options, {
expiresIn: 900,
})
})
})
})
Loading
Loading