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();