253 lines
6.7 KiB
TypeScript
253 lines
6.7 KiB
TypeScript
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
|
import {
|
||
|
|
S3Client,
|
||
|
|
CreateBucketCommand,
|
||
|
|
DeleteBucketCommand,
|
||
|
|
PutBucketPolicyCommand,
|
||
|
|
GetBucketPolicyCommand,
|
||
|
|
DeleteBucketPolicyCommand,
|
||
|
|
} from '@aws-sdk/client-s3';
|
||
|
|
import * as smarts3 from '../ts/index.js';
|
||
|
|
|
||
|
|
let testSmarts3Instance: smarts3.Smarts3;
|
||
|
|
let authClient: S3Client;
|
||
|
|
|
||
|
|
const TEST_PORT = 3345;
|
||
|
|
const ACCESS_KEY = 'TESTAKID';
|
||
|
|
const SECRET_KEY = 'TESTSECRETKEY123';
|
||
|
|
const BUCKET = 'policy-crud-bucket';
|
||
|
|
|
||
|
|
function makePolicy(statements: any[]) {
|
||
|
|
return JSON.stringify({ Version: '2012-10-17', Statement: statements });
|
||
|
|
}
|
||
|
|
|
||
|
|
const validStatement = {
|
||
|
|
Sid: 'Test1',
|
||
|
|
Effect: 'Allow',
|
||
|
|
Principal: '*',
|
||
|
|
Action: ['s3:GetObject'],
|
||
|
|
Resource: [`arn:aws:s3:::${BUCKET}/*`],
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================
|
||
|
|
// Server setup
|
||
|
|
// ============================
|
||
|
|
|
||
|
|
tap.test('setup: start S3 server with auth enabled', async () => {
|
||
|
|
testSmarts3Instance = await smarts3.Smarts3.createAndStart({
|
||
|
|
server: { port: TEST_PORT, silent: true, region: 'us-east-1' },
|
||
|
|
storage: { cleanSlate: true },
|
||
|
|
auth: {
|
||
|
|
enabled: true,
|
||
|
|
credentials: [{ accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY }],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
authClient = new S3Client({
|
||
|
|
endpoint: `http://localhost:${TEST_PORT}`,
|
||
|
|
region: 'us-east-1',
|
||
|
|
credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY },
|
||
|
|
forcePathStyle: true,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('setup: create bucket', async () => {
|
||
|
|
await authClient.send(new CreateBucketCommand({ Bucket: BUCKET }));
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================
|
||
|
|
// CRUD tests
|
||
|
|
// ============================
|
||
|
|
|
||
|
|
tap.test('GET policy on bucket with no policy → throws (NoSuchBucketPolicy)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(new GetBucketPolicyCommand({ Bucket: BUCKET }))
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT valid policy → 204', async () => {
|
||
|
|
const response = await authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: makePolicy([validStatement]),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(204);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('GET policy back → returns matching JSON', async () => {
|
||
|
|
const response = await authClient.send(
|
||
|
|
new GetBucketPolicyCommand({ Bucket: BUCKET })
|
||
|
|
);
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||
|
|
const policy = JSON.parse(response.Policy!);
|
||
|
|
expect(policy.Version).toEqual('2012-10-17');
|
||
|
|
expect(policy.Statement[0].Sid).toEqual('Test1');
|
||
|
|
expect(policy.Statement[0].Effect).toEqual('Allow');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT updated policy (overwrite) → 204, GET returns new version', async () => {
|
||
|
|
const updatedStatement = {
|
||
|
|
Sid: 'Updated',
|
||
|
|
Effect: 'Deny',
|
||
|
|
Principal: '*',
|
||
|
|
Action: ['s3:DeleteObject'],
|
||
|
|
Resource: [`arn:aws:s3:::${BUCKET}/*`],
|
||
|
|
};
|
||
|
|
|
||
|
|
const putResp = await authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: makePolicy([updatedStatement]),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
expect(putResp.$metadata.httpStatusCode).toEqual(204);
|
||
|
|
|
||
|
|
const getResp = await authClient.send(
|
||
|
|
new GetBucketPolicyCommand({ Bucket: BUCKET })
|
||
|
|
);
|
||
|
|
const policy = JSON.parse(getResp.Policy!);
|
||
|
|
expect(policy.Statement[0].Sid).toEqual('Updated');
|
||
|
|
expect(policy.Statement[0].Effect).toEqual('Deny');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('DELETE policy → 204', async () => {
|
||
|
|
const response = await authClient.send(
|
||
|
|
new DeleteBucketPolicyCommand({ Bucket: BUCKET })
|
||
|
|
);
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(204);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('DELETE policy again (idempotent) → 204', async () => {
|
||
|
|
const response = await authClient.send(
|
||
|
|
new DeleteBucketPolicyCommand({ Bucket: BUCKET })
|
||
|
|
);
|
||
|
|
expect(response.$metadata.httpStatusCode).toEqual(204);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('GET policy after delete → throws', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(new GetBucketPolicyCommand({ Bucket: BUCKET }))
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT policy on non-existent bucket → throws (NoSuchBucket)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: 'nonexistent-bucket-xyz',
|
||
|
|
Policy: makePolicy([validStatement]),
|
||
|
|
})
|
||
|
|
)
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT invalid JSON → throws (MalformedPolicy)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: '{not valid json!!!',
|
||
|
|
})
|
||
|
|
)
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT policy with wrong version → throws (MalformedPolicy)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: JSON.stringify({
|
||
|
|
Version: '2023-01-01',
|
||
|
|
Statement: [validStatement],
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
)
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT policy with empty statements array → throws (MalformedPolicy)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: JSON.stringify({
|
||
|
|
Version: '2012-10-17',
|
||
|
|
Statement: [],
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
)
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT policy with action missing s3: prefix → throws (MalformedPolicy)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: makePolicy([
|
||
|
|
{
|
||
|
|
Sid: 'BadAction',
|
||
|
|
Effect: 'Allow',
|
||
|
|
Principal: '*',
|
||
|
|
Action: ['GetObject'],
|
||
|
|
Resource: [`arn:aws:s3:::${BUCKET}/*`],
|
||
|
|
},
|
||
|
|
]),
|
||
|
|
})
|
||
|
|
)
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('PUT policy with resource missing arn:aws:s3::: prefix → throws (MalformedPolicy)', async () => {
|
||
|
|
await expect(
|
||
|
|
authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: makePolicy([
|
||
|
|
{
|
||
|
|
Sid: 'BadResource',
|
||
|
|
Effect: 'Allow',
|
||
|
|
Principal: '*',
|
||
|
|
Action: ['s3:GetObject'],
|
||
|
|
Resource: ['policy-crud-bucket/*'],
|
||
|
|
},
|
||
|
|
]),
|
||
|
|
})
|
||
|
|
)
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('Bucket deletion cleans up associated policy', async () => {
|
||
|
|
// PUT a policy
|
||
|
|
await authClient.send(
|
||
|
|
new PutBucketPolicyCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Policy: makePolicy([validStatement]),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
// Delete the bucket
|
||
|
|
await authClient.send(new DeleteBucketCommand({ Bucket: BUCKET }));
|
||
|
|
|
||
|
|
// Re-create the bucket
|
||
|
|
await authClient.send(new CreateBucketCommand({ Bucket: BUCKET }));
|
||
|
|
|
||
|
|
// GET policy should now be gone
|
||
|
|
await expect(
|
||
|
|
authClient.send(new GetBucketPolicyCommand({ Bucket: BUCKET }))
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================
|
||
|
|
// Teardown
|
||
|
|
// ============================
|
||
|
|
|
||
|
|
tap.test('teardown: delete bucket and stop server', async () => {
|
||
|
|
await authClient.send(new DeleteBucketCommand({ Bucket: BUCKET }));
|
||
|
|
await testSmarts3Instance.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|