151 lines
5.1 KiB
TypeScript
151 lines
5.1 KiB
TypeScript
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
|
import {
|
||
|
|
CreateBucketCommand,
|
||
|
|
DeleteBucketCommand,
|
||
|
|
ListBucketsCommand,
|
||
|
|
S3Client,
|
||
|
|
} from '@aws-sdk/client-s3';
|
||
|
|
import * as smartstorage from '../ts/index.js';
|
||
|
|
|
||
|
|
const TEST_PORT = 3349;
|
||
|
|
const INITIAL_CREDENTIAL: smartstorage.IStorageCredential = {
|
||
|
|
accessKeyId: 'RUNTIMEINITIAL',
|
||
|
|
secretAccessKey: 'RUNTIMEINITIALSECRET123',
|
||
|
|
};
|
||
|
|
const ROTATED_CREDENTIAL_A: smartstorage.IStorageCredential = {
|
||
|
|
accessKeyId: 'RUNTIMEA',
|
||
|
|
secretAccessKey: 'RUNTIMEASECRET123',
|
||
|
|
};
|
||
|
|
const ROTATED_CREDENTIAL_B: smartstorage.IStorageCredential = {
|
||
|
|
accessKeyId: 'RUNTIMEB',
|
||
|
|
secretAccessKey: 'RUNTIMEBSECRET123',
|
||
|
|
};
|
||
|
|
const TEST_BUCKET = 'runtime-credentials-bucket';
|
||
|
|
|
||
|
|
let testSmartStorageInstance: smartstorage.SmartStorage;
|
||
|
|
let initialClient: S3Client;
|
||
|
|
let rotatedClientA: S3Client;
|
||
|
|
let rotatedClientB: S3Client;
|
||
|
|
|
||
|
|
function createS3Client(credential: smartstorage.IStorageCredential): S3Client {
|
||
|
|
return new S3Client({
|
||
|
|
endpoint: `http://localhost:${TEST_PORT}`,
|
||
|
|
region: 'us-east-1',
|
||
|
|
credentials: {
|
||
|
|
accessKeyId: credential.accessKeyId,
|
||
|
|
secretAccessKey: credential.secretAccessKey,
|
||
|
|
},
|
||
|
|
forcePathStyle: true,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
tap.test('setup: start storage server with runtime-managed credentials', async () => {
|
||
|
|
testSmartStorageInstance = await smartstorage.SmartStorage.createAndStart({
|
||
|
|
server: {
|
||
|
|
port: TEST_PORT,
|
||
|
|
silent: true,
|
||
|
|
region: 'us-east-1',
|
||
|
|
},
|
||
|
|
storage: {
|
||
|
|
cleanSlate: true,
|
||
|
|
},
|
||
|
|
auth: {
|
||
|
|
enabled: true,
|
||
|
|
credentials: [INITIAL_CREDENTIAL],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
initialClient = createS3Client(INITIAL_CREDENTIAL);
|
||
|
|
rotatedClientA = createS3Client(ROTATED_CREDENTIAL_A);
|
||
|
|
rotatedClientB = createS3Client(ROTATED_CREDENTIAL_B);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('startup credentials authenticate successfully', async () => {
|
||
|
|
const response = await initialClient.send(new ListBucketsCommand({}));
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('listCredentials returns the active startup credential set', async () => {
|
||
|
|
const credentials = await testSmartStorageInstance.listCredentials();
|
||
|
|
expect(credentials.length).toEqual(1);
|
||
|
|
expect(credentials[0].accessKeyId).toEqual(INITIAL_CREDENTIAL.accessKeyId);
|
||
|
|
expect(credentials[0].secretAccessKey).toEqual(INITIAL_CREDENTIAL.secretAccessKey);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('invalid replacement input fails cleanly and leaves old credentials active', async () => {
|
||
|
|
await expect(
|
||
|
|
testSmartStorageInstance.replaceCredentials([
|
||
|
|
{
|
||
|
|
accessKeyId: '',
|
||
|
|
secretAccessKey: 'invalid-secret',
|
||
|
|
},
|
||
|
|
]),
|
||
|
|
).rejects.toThrow();
|
||
|
|
|
||
|
|
const credentials = await testSmartStorageInstance.listCredentials();
|
||
|
|
expect(credentials.length).toEqual(1);
|
||
|
|
expect(credentials[0].accessKeyId).toEqual(INITIAL_CREDENTIAL.accessKeyId);
|
||
|
|
|
||
|
|
const response = await initialClient.send(new ListBucketsCommand({}));
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('replacing credentials swaps the active set atomically', async () => {
|
||
|
|
await testSmartStorageInstance.replaceCredentials([
|
||
|
|
ROTATED_CREDENTIAL_A,
|
||
|
|
ROTATED_CREDENTIAL_B,
|
||
|
|
]);
|
||
|
|
|
||
|
|
const credentials = await testSmartStorageInstance.listCredentials();
|
||
|
|
expect(credentials.length).toEqual(2);
|
||
|
|
expect(credentials[0].accessKeyId).toEqual(ROTATED_CREDENTIAL_A.accessKeyId);
|
||
|
|
expect(credentials[1].accessKeyId).toEqual(ROTATED_CREDENTIAL_B.accessKeyId);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('old credentials stop working immediately for new requests', async () => {
|
||
|
|
await expect(initialClient.send(new ListBucketsCommand({}))).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('first rotated credential authenticates successfully', async () => {
|
||
|
|
const response = await rotatedClientA.send(
|
||
|
|
new CreateBucketCommand({ Bucket: TEST_BUCKET }),
|
||
|
|
);
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('multiple rotated credentials remain active', async () => {
|
||
|
|
const response = await rotatedClientB.send(new ListBucketsCommand({}));
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||
|
|
expect(response.Buckets?.some((bucket) => bucket.Name === TEST_BUCKET)).toEqual(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('duplicate replacement input fails cleanly without changing the active set', async () => {
|
||
|
|
await expect(
|
||
|
|
testSmartStorageInstance.replaceCredentials([
|
||
|
|
ROTATED_CREDENTIAL_A,
|
||
|
|
{
|
||
|
|
accessKeyId: ROTATED_CREDENTIAL_A.accessKeyId,
|
||
|
|
secretAccessKey: 'another-secret',
|
||
|
|
},
|
||
|
|
]),
|
||
|
|
).rejects.toThrow();
|
||
|
|
|
||
|
|
const credentials = await testSmartStorageInstance.listCredentials();
|
||
|
|
expect(credentials.length).toEqual(2);
|
||
|
|
expect(credentials[0].accessKeyId).toEqual(ROTATED_CREDENTIAL_A.accessKeyId);
|
||
|
|
expect(credentials[1].accessKeyId).toEqual(ROTATED_CREDENTIAL_B.accessKeyId);
|
||
|
|
|
||
|
|
const response = await rotatedClientA.send(new ListBucketsCommand({}));
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('teardown: clean up bucket and stop the storage server', async () => {
|
||
|
|
const response = await rotatedClientA.send(
|
||
|
|
new DeleteBucketCommand({ Bucket: TEST_BUCKET }),
|
||
|
|
);
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(204);
|
||
|
|
await testSmartStorageInstance.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start()
|