import { expect, tap } from '@git.zone/tstest/tapbundle'; import { S3Client, CreateBucketCommand, ListBucketsCommand, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteBucketCommand, PutBucketPolicyCommand, GetBucketPolicyCommand, DeleteBucketPolicyCommand, } from '@aws-sdk/client-s3'; import { Readable } from 'stream'; import * as smarts3 from '../ts/index.js'; let testSmarts3Instance: smarts3.Smarts3; let authClient: S3Client; let wrongClient: S3Client; const TEST_PORT = 3344; const ACCESS_KEY = 'TESTAKID'; const SECRET_KEY = 'TESTSECRETKEY123'; async function streamToString(stream: Readable): Promise { const chunks: Buffer[] = []; return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); stream.on('error', reject); stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); }); } // ============================ // Server setup // ============================ tap.test('should 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, }, ], }, }); // Authenticated client with correct credentials authClient = new S3Client({ endpoint: `http://localhost:${TEST_PORT}`, region: 'us-east-1', credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY, }, forcePathStyle: true, }); // Client with wrong credentials wrongClient = new S3Client({ endpoint: `http://localhost:${TEST_PORT}`, region: 'us-east-1', credentials: { accessKeyId: 'WRONGKEY', secretAccessKey: 'WRONGSECRET', }, forcePathStyle: true, }); }); // ============================ // Authenticated CRUD // ============================ tap.test('authenticated: should list buckets', async () => { const response = await authClient.send(new ListBucketsCommand({})); expect(response.$metadata.httpStatusCode).toEqual(200); expect(Array.isArray(response.Buckets)).toEqual(true); }); tap.test('authenticated: should create a bucket', async () => { const response = await authClient.send(new CreateBucketCommand({ Bucket: 'auth-test-bucket' })); expect(response.$metadata.httpStatusCode).toEqual(200); }); tap.test('authenticated: should upload an object', async () => { const response = await authClient.send( new PutObjectCommand({ Bucket: 'auth-test-bucket', Key: 'hello.txt', Body: 'Hello authenticated world!', ContentType: 'text/plain', }), ); expect(response.$metadata.httpStatusCode).toEqual(200); }); tap.test('authenticated: should download the object', async () => { const response = await authClient.send( new GetObjectCommand({ Bucket: 'auth-test-bucket', Key: 'hello.txt', }), ); expect(response.$metadata.httpStatusCode).toEqual(200); const content = await streamToString(response.Body as Readable); expect(content).toEqual('Hello authenticated world!'); }); // ============================ // Wrong credentials → 403 // ============================ tap.test('wrong credentials: should fail to list buckets', async () => { await expect(wrongClient.send(new ListBucketsCommand({}))).rejects.toThrow(); }); tap.test('wrong credentials: should fail to get object', async () => { await expect( wrongClient.send( new GetObjectCommand({ Bucket: 'auth-test-bucket', Key: 'hello.txt', }), ), ).rejects.toThrow(); }); // ============================ // Anonymous → 403 (no policy yet) // ============================ tap.test('anonymous: should fail to list buckets', async () => { const resp = await fetch(`http://localhost:${TEST_PORT}/`); expect(resp.status).toEqual(403); }); tap.test('anonymous: should fail to get object (no policy)', async () => { const resp = await fetch(`http://localhost:${TEST_PORT}/auth-test-bucket/hello.txt`); expect(resp.status).toEqual(403); }); // ============================ // Bucket policy: public read // ============================ tap.test('should PUT a public-read bucket policy', async () => { const policy = { Version: '2012-10-17', Statement: [ { Sid: 'PublicRead', Effect: 'Allow', Principal: '*', Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::auth-test-bucket/*`], }, ], }; const response = await authClient.send( new PutBucketPolicyCommand({ Bucket: 'auth-test-bucket', Policy: JSON.stringify(policy), }), ); expect(response.$metadata.httpStatusCode).toEqual(204); }); tap.test('should GET the bucket policy', async () => { const response = await authClient.send( new GetBucketPolicyCommand({ Bucket: 'auth-test-bucket', }), ); expect(response.$metadata.httpStatusCode).toEqual(200); const policy = JSON.parse(response.Policy!); expect(policy.Statement[0].Sid).toEqual('PublicRead'); }); tap.test('anonymous: should GET object after public-read policy', async () => { const resp = await fetch(`http://localhost:${TEST_PORT}/auth-test-bucket/hello.txt`); expect(resp.status).toEqual(200); const content = await resp.text(); expect(content).toEqual('Hello authenticated world!'); }); tap.test('anonymous: should still fail to PUT object (policy only allows GET)', async () => { const resp = await fetch(`http://localhost:${TEST_PORT}/auth-test-bucket/anon-file.txt`, { method: 'PUT', body: 'should fail', }); expect(resp.status).toEqual(403); }); // ============================ // Deny policy // ============================ tap.test('should PUT a deny policy that blocks authenticated delete', async () => { const policy = { Version: '2012-10-17', Statement: [ { Sid: 'PublicRead', Effect: 'Allow', Principal: '*', Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::auth-test-bucket/*`], }, { Sid: 'DenyDelete', Effect: 'Deny', Principal: '*', Action: ['s3:DeleteObject'], Resource: [`arn:aws:s3:::auth-test-bucket/*`], }, ], }; const response = await authClient.send( new PutBucketPolicyCommand({ Bucket: 'auth-test-bucket', Policy: JSON.stringify(policy), }), ); expect(response.$metadata.httpStatusCode).toEqual(204); }); tap.test('authenticated: should be denied delete by policy', async () => { await expect( authClient.send( new DeleteObjectCommand({ Bucket: 'auth-test-bucket', Key: 'hello.txt', }), ), ).rejects.toThrow(); }); // ============================ // DELETE bucket policy // ============================ tap.test('should DELETE the bucket policy', async () => { const response = await authClient.send( new DeleteBucketPolicyCommand({ Bucket: 'auth-test-bucket', }), ); expect(response.$metadata.httpStatusCode).toEqual(204); }); tap.test('should GET policy → 404 after deletion', async () => { await expect( authClient.send( new GetBucketPolicyCommand({ Bucket: 'auth-test-bucket', }), ), ).rejects.toThrow(); }); // ============================ // Cleanup // ============================ tap.test('authenticated: delete object after policy removed', async () => { const response = await authClient.send( new DeleteObjectCommand({ Bucket: 'auth-test-bucket', Key: 'hello.txt', }), ); expect(response.$metadata.httpStatusCode).toEqual(204); }); tap.test('authenticated: delete the bucket', async () => { const response = await authClient.send( new DeleteBucketCommand({ Bucket: 'auth-test-bucket' }), ); expect(response.$metadata.httpStatusCode).toEqual(204); }); tap.test('should stop the S3 server', async () => { await testSmarts3Instance.stop(); }); export default tap.start();