Files
smarts3/test/test.auth.node.ts

302 lines
8.0 KiB
TypeScript
Raw Normal View History

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