feat: implement authentication routes with login, logout, and user info retrieval
feat: add S3 bucket versioning support and related XML response handling refactor: rename temporary file paths from 'teleuploader' to 'filedrop' for consistency fix: update Swagger documentation to reflect new API name and descriptions test: add unit tests for authentication routes and utilities test: implement end-to-end tests for S3 bucket configuration and versioning chore: update environment variable defaults for new service name
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
const defaultEnv = (key: string, value: string) => {
|
||||
process.env[key] ||= value;
|
||||
};
|
||||
const setEnv = (key: string, value: string) => {
|
||||
process.env[key] = value;
|
||||
};
|
||||
|
||||
defaultEnv('BOT_TOKEN', '123456:ABC-DEF');
|
||||
defaultEnv('STORAGE_CHANNEL_ID', '-1001234567890');
|
||||
defaultEnv('BASE_URL', 'https://example.com');
|
||||
defaultEnv('DATABASE_URL', 'postgresql://user:pass@localhost:5432/test');
|
||||
defaultEnv('PORT', '3000');
|
||||
defaultEnv('NODE_ENV', 'test');
|
||||
setEnv('ADMIN_API_TOKEN', 'route-secret-token');
|
||||
setEnv('SESSION_COOKIE_NAME', 'route_session');
|
||||
setEnv('SESSION_COOKIE_MAX_AGE_SECONDS', '3600');
|
||||
|
||||
const { createSessionCookie } = await import('../src/utils/auth');
|
||||
const { handleLogin, handleLogout, handleMe } = await import('../src/routes/auth');
|
||||
|
||||
const jsonBody = async <T>(res: Response): Promise<T> => (await res.json()) as T;
|
||||
|
||||
describe('auth routes', () => {
|
||||
it('logs in with the configured token and sets a session cookie', async () => {
|
||||
const res = await handleLogin(
|
||||
new Request('http://localhost/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'route-secret-token' }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await jsonBody<{ username: string }>(res)).toEqual({ username: 'admin' });
|
||||
expect(res.headers.get('set-cookie')).toContain('route_session=');
|
||||
expect(res.headers.get('set-cookie')).toContain('HttpOnly');
|
||||
});
|
||||
|
||||
it('rejects a wrong login token', async () => {
|
||||
const res = await handleLogin(
|
||||
new Request('http://localhost/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'wrong' }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(await jsonBody<{ error: string }>(res)).toEqual({ error: 'Invalid token' });
|
||||
expect(res.headers.get('set-cookie')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects missing or invalid login body', async () => {
|
||||
const res = await handleLogin(
|
||||
new Request('http://localhost/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(await jsonBody<{ error: string }>(res)).toEqual({ error: 'Token is required' });
|
||||
});
|
||||
|
||||
it('returns the current user from a valid session cookie', async () => {
|
||||
const setCookie = createSessionCookie('admin');
|
||||
const cookieHeader = setCookie.split(';')[0];
|
||||
|
||||
const res = await handleMe(
|
||||
new Request('http://localhost/api/v1/auth/me', {
|
||||
headers: { cookie: cookieHeader },
|
||||
}),
|
||||
);
|
||||
|
||||
const body = await jsonBody<{ username: string; expiresAt: string | null }>(res);
|
||||
expect(res.status).toBe(200);
|
||||
expect(body.username).toBe('admin');
|
||||
if (typeof body.expiresAt !== 'string') throw new Error('Expected expiresAt string');
|
||||
expect(new Date(body.expiresAt).getTime()).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it('returns the current user from a bearer token', async () => {
|
||||
const res = await handleMe(
|
||||
new Request('http://localhost/api/v1/auth/me', {
|
||||
headers: { authorization: 'Bearer route-secret-token' },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await jsonBody<{ username: string; expiresAt: null }>(res)).toEqual({
|
||||
username: 'admin',
|
||||
expiresAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects missing and tampered sessions', async () => {
|
||||
const missing = await handleMe(new Request('http://localhost/api/v1/auth/me'));
|
||||
expect(missing.status).toBe(401);
|
||||
|
||||
const validCookie = createSessionCookie('admin').split(';')[0];
|
||||
const tamperedCookie = `${validCookie}x`;
|
||||
const tampered = await handleMe(
|
||||
new Request('http://localhost/api/v1/auth/me', {
|
||||
headers: { cookie: tamperedCookie },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tampered.status).toBe(401);
|
||||
});
|
||||
|
||||
it('clears the session on logout', async () => {
|
||||
const res = await handleLogout();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await jsonBody<{ success: boolean }>(res)).toEqual({ success: true });
|
||||
expect(res.headers.get('set-cookie')).toContain('route_session=;');
|
||||
expect(res.headers.get('set-cookie')).toContain('Max-Age=0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
const defaultEnv = (key: string, value: string) => {
|
||||
process.env[key] ||= value;
|
||||
};
|
||||
const setEnv = (key: string, value: string) => {
|
||||
process.env[key] = value;
|
||||
};
|
||||
|
||||
defaultEnv('BOT_TOKEN', '123456:ABC-DEF');
|
||||
defaultEnv('STORAGE_CHANNEL_ID', '-1001234567890');
|
||||
defaultEnv('BASE_URL', 'https://example.com');
|
||||
defaultEnv('DATABASE_URL', 'postgresql://user:pass@localhost:5432/test');
|
||||
defaultEnv('PORT', '3000');
|
||||
defaultEnv('NODE_ENV', 'test');
|
||||
setEnv('ADMIN_API_TOKEN', 'route-secret-token');
|
||||
setEnv('SESSION_COOKIE_NAME', 'route_session');
|
||||
setEnv('SESSION_COOKIE_MAX_AGE_SECONDS', '3600');
|
||||
|
||||
const auth = await import('../src/utils/auth');
|
||||
|
||||
describe('auth utilities', () => {
|
||||
const secret = 'admin-secret-token';
|
||||
const cookieName = 'test_session';
|
||||
|
||||
it('signs and verifies a cookie payload', () => {
|
||||
const payload = Buffer.from(JSON.stringify({ u: 'admin', e: Date.now() + 60_000 })).toString(
|
||||
'base64url',
|
||||
);
|
||||
const signature = auth.signCookiePayload(payload, secret);
|
||||
|
||||
expect(auth.verifyCookieSignature(`${payload}.${signature}`, secret)).toBe(payload);
|
||||
});
|
||||
|
||||
it('rejects tampered cookie signatures', () => {
|
||||
const payload = Buffer.from(JSON.stringify({ u: 'admin', e: Date.now() + 60_000 })).toString(
|
||||
'base64url',
|
||||
);
|
||||
const signature = auth.signCookiePayload(payload, secret);
|
||||
|
||||
expect(auth.verifyCookieSignature(`${payload}x.${signature}`, secret)).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects malformed cookie values', () => {
|
||||
expect(auth.verifyCookieSignature('', secret)).toBeNull();
|
||||
expect(auth.verifyCookieSignature('payload-only', secret)).toBeNull();
|
||||
expect(auth.verifyCookieSignature('.signature', secret)).toBeNull();
|
||||
expect(auth.verifyCookieSignature('payload.', secret)).toBeNull();
|
||||
});
|
||||
|
||||
it('creates a secure HttpOnly session cookie', () => {
|
||||
const cookie = auth.createSessionCookie('admin', {
|
||||
secret,
|
||||
cookieName,
|
||||
maxAgeMs: 60_000,
|
||||
});
|
||||
|
||||
expect(cookie).toStartWith(`${cookieName}=`);
|
||||
expect(cookie).toContain('Max-Age=60');
|
||||
expect(cookie).toContain('Path=/');
|
||||
expect(cookie).toContain('HttpOnly');
|
||||
expect(cookie).toContain('SameSite=Lax');
|
||||
expect(cookie).toContain('Secure');
|
||||
});
|
||||
|
||||
it('clears a session cookie', () => {
|
||||
const cookie = auth.clearSessionCookie(cookieName);
|
||||
|
||||
expect(cookie).toStartWith(`${cookieName}=;`);
|
||||
expect(cookie).toContain('Max-Age=0');
|
||||
expect(cookie).toContain('HttpOnly');
|
||||
});
|
||||
|
||||
it('parses a valid signed session cookie', () => {
|
||||
const setCookie = auth.createSessionCookie('admin', {
|
||||
secret,
|
||||
cookieName,
|
||||
maxAgeMs: 60_000,
|
||||
});
|
||||
const cookieHeader = setCookie.split(';')[0];
|
||||
|
||||
const session = auth.parseSessionFromCookie(cookieHeader, { secret, cookieName });
|
||||
|
||||
expect(session?.username).toBe('admin');
|
||||
expect(session?.method).toBe('cookie');
|
||||
expect(session?.expiresAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('rejects an expired session cookie', () => {
|
||||
const expiredPayload = Buffer.from(
|
||||
JSON.stringify({ u: 'admin', e: Date.now() - 1_000 }),
|
||||
).toString('base64url');
|
||||
const signature = auth.signCookiePayload(expiredPayload, secret);
|
||||
|
||||
const session = auth.parseSessionFromCookie(`${cookieName}=${expiredPayload}.${signature}`, {
|
||||
secret,
|
||||
cookieName,
|
||||
});
|
||||
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects cookies with the wrong name or secret', () => {
|
||||
const setCookie = auth.createSessionCookie('admin', {
|
||||
secret,
|
||||
cookieName,
|
||||
maxAgeMs: 60_000,
|
||||
});
|
||||
const cookieHeader = setCookie.split(';')[0];
|
||||
|
||||
expect(auth.parseSessionFromCookie(cookieHeader, { secret, cookieName: 'other' })).toBeNull();
|
||||
expect(auth.parseSessionFromCookie(cookieHeader, { secret: 'wrong', cookieName })).toBeNull();
|
||||
});
|
||||
|
||||
it('validates bearer tokens', () => {
|
||||
expect(auth.checkBearerToken(`Bearer ${secret}`, secret)).toBe(true);
|
||||
expect(auth.checkBearerToken('Bearer wrong', secret)).toBe(false);
|
||||
expect(auth.checkBearerToken(secret, secret)).toBe(false);
|
||||
expect(auth.checkBearerToken(null, secret)).toBe(false);
|
||||
});
|
||||
|
||||
it('uses cookie and bearer auth for request sessions', () => {
|
||||
const setCookie = auth.createSessionCookie('admin', {
|
||||
secret,
|
||||
cookieName,
|
||||
maxAgeMs: 60_000,
|
||||
});
|
||||
const cookieRequest = new Request('http://localhost/api', {
|
||||
headers: { cookie: setCookie.split(';')[0] },
|
||||
});
|
||||
const bearerRequest = new Request('http://localhost/api', {
|
||||
headers: { authorization: `Bearer ${secret}` },
|
||||
});
|
||||
|
||||
expect(auth.getAuthSession(cookieRequest, { secret, cookieName })?.method).toBe('cookie');
|
||||
expect(auth.getAuthSession(bearerRequest, { secret, cookieName })?.method).toBe('bearer');
|
||||
});
|
||||
|
||||
it('allows protected handlers when auth is disabled', async () => {
|
||||
let calls = 0;
|
||||
const handler = (_req: Request) => {
|
||||
calls++;
|
||||
return Response.json({ ok: true });
|
||||
};
|
||||
const protectedHandler = auth.requireAuth(handler, { secret: '', cookieName });
|
||||
|
||||
const res = await protectedHandler(new Request('http://localhost/api'));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
it('rejects protected handlers without a valid session', async () => {
|
||||
let calls = 0;
|
||||
const handler = (_req: Request) => {
|
||||
calls++;
|
||||
return Response.json({ ok: true });
|
||||
};
|
||||
const protectedHandler = auth.requireAuth(handler, { secret, cookieName });
|
||||
|
||||
const res = await protectedHandler(new Request('http://localhost/api'));
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: 'Unauthorized' });
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it('passes protected handlers with a bearer token', async () => {
|
||||
let calls = 0;
|
||||
const handler = (_req: Request) => {
|
||||
calls++;
|
||||
return Response.json({ ok: true });
|
||||
};
|
||||
const protectedHandler = auth.requireAuth(handler, { secret, cookieName });
|
||||
|
||||
const res = await protectedHandler(
|
||||
new Request('http://localhost/api', { headers: { authorization: `Bearer ${secret}` } }),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,12 @@ mock.module('../src/routes/health', () => ({
|
||||
handleHealth: mock(),
|
||||
}));
|
||||
|
||||
mock.module('../src/routes/auth', () => ({
|
||||
handleLogin: mock(),
|
||||
handleLogout: mock(),
|
||||
handleMe: mock(),
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/rateLimit', () => ({
|
||||
cleanupRateLimitCache: mock(),
|
||||
withRateLimit: <T extends Request>(
|
||||
@@ -76,5 +82,9 @@ describe('Bootstrap Server', () => {
|
||||
expect(serveCallArgs.routes).toHaveProperty('/f/:public_id');
|
||||
expect(serveCallArgs.routes).toHaveProperty('/file/:public_id/info');
|
||||
expect(serveCallArgs.routes).toHaveProperty('/health');
|
||||
expect(serveCallArgs.routes).toHaveProperty('/api/v1/auth/login');
|
||||
expect(serveCallArgs.routes).toHaveProperty('/api/v1/auth/logout');
|
||||
expect(serveCallArgs.routes).toHaveProperty('/api/v1/auth/me');
|
||||
expect(serveCallArgs.routes).toHaveProperty('/api/v1/*');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,9 @@ describe('Environment Variables Validation', () => {
|
||||
expect(config).toHaveProperty('logLevel');
|
||||
expect(config).toHaveProperty('rateLimitWindowMs');
|
||||
expect(config).toHaveProperty('rateLimitMaxRequests');
|
||||
expect(config).toHaveProperty('adminApiToken');
|
||||
expect(config).toHaveProperty('sessionCookieName');
|
||||
expect(config).toHaveProperty('sessionMaxAgeMs');
|
||||
});
|
||||
|
||||
it('config.botToken should return BOT_TOKEN from process.env', () => {
|
||||
@@ -42,4 +45,10 @@ describe('Environment Variables Validation', () => {
|
||||
it('rateLimitMaxRequests should default to 150 when not specified', () => {
|
||||
expect(config.rateLimitMaxRequests).toBe(150);
|
||||
});
|
||||
|
||||
it('auth config should be disabled by default with a 24 hour session', () => {
|
||||
expect(config.adminApiToken).toBe(process.env.ADMIN_API_TOKEN || '');
|
||||
expect(config.sessionCookieName).toBe(process.env.SESSION_COOKIE_NAME || 'tu_session');
|
||||
expect(config.sessionMaxAgeMs).toBe(86400 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Tests both the Web API (JSON v1) and S3 (SigV4 XML) interfaces.
|
||||
* Requires env vars:
|
||||
* - BASE_URL (default: https://upload.asepharyana.my.id)
|
||||
* - S3_ACCESS_KEY (default: teleuploader-admin)
|
||||
* - S3_ACCESS_KEY (default: filedrop-admin)
|
||||
* - S3_SECRET_KEY (required for S3 tests)
|
||||
*
|
||||
* Usage:
|
||||
@@ -15,7 +15,7 @@ import { afterAll, describe, expect, it } from 'bun:test';
|
||||
|
||||
// ── Config ───────────────────────────────────────────────────────────────────
|
||||
const BASE_URL = process.env.BASE_URL || 'https://upload.asepharyana.my.id';
|
||||
const S3_KEY = process.env.S3_ACCESS_KEY || 'teleuploader-admin';
|
||||
const S3_KEY = process.env.S3_ACCESS_KEY || 'filedrop-admin';
|
||||
const S3_SECRET = process.env.S3_SECRET_KEY || '';
|
||||
|
||||
const TS = Date.now().toString(36);
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('S3 Auth (SigV4)', () => {
|
||||
.join('');
|
||||
|
||||
it('verifies presigned GET using the public request host', async () => {
|
||||
const accessKey = 'teleuploader-admin';
|
||||
const accessKey = 'filedrop-admin';
|
||||
const secret = 'unit-test-secret';
|
||||
const host = 'upload.example.test';
|
||||
const path = '/bucket/key.txt';
|
||||
@@ -152,10 +152,10 @@ describe('S3 Auth (SigV4)', () => {
|
||||
|
||||
it('rejects presigned URLs signed for a different host', async () => {
|
||||
const result = await verifyPresignedUrl({
|
||||
url: 'https://wrong.example.test/bucket/key.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=teleuploader-admin%2F20260707%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260707T120000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=00',
|
||||
url: 'https://wrong.example.test/bucket/key.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=filedrop-admin%2F20260707%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260707T120000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=00',
|
||||
method: 'GET',
|
||||
headers: { host: 'upload.example.test' },
|
||||
s3AccessKey: 'teleuploader-admin',
|
||||
s3AccessKey: 'filedrop-admin',
|
||||
s3SecretKey: 'unit-test-secret',
|
||||
region: 'us-east-1',
|
||||
now: new Date('2026-07-07T12:05:00Z'),
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.BOT_TOKEN = '123456:ABC-DEF';
|
||||
process.env.STORAGE_CHANNEL_ID = '-1001234567890';
|
||||
process.env.BASE_URL = 'http://localhost:3000';
|
||||
process.env.DATABASE_URL = 'postgresql://localhost/test';
|
||||
process.env.PORT = '3000';
|
||||
process.env.S3_ACCESS_KEY = 'filedrop-admin';
|
||||
process.env.S3_SECRET_KEY = 'unit-test-secret';
|
||||
|
||||
const bucket = {
|
||||
id: 'bucket-uuid',
|
||||
name: 'gitea',
|
||||
createdAt: new Date('2026-01-01T00:00:00Z'),
|
||||
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
mock.module('../src/db/buckets', () => ({
|
||||
createBucket: () => Promise.resolve(bucket),
|
||||
deleteBucket: () => Promise.resolve(true),
|
||||
findBucketByName: (name: string) => Promise.resolve(name === bucket.name ? bucket : null),
|
||||
listBuckets: () => Promise.resolve([bucket]),
|
||||
}));
|
||||
|
||||
mock.module('../src/db/files-ext', () => ({
|
||||
countBucketObjects: () => Promise.resolve(0),
|
||||
findFileByBucketAndKey: () => Promise.resolve(null),
|
||||
listObjectsByPrefix: () => Promise.resolve({ objects: [], prefixes: [] }),
|
||||
softDeleteFile: () => Promise.resolve(true),
|
||||
}));
|
||||
|
||||
mock.module('../src/db/multipart', () => ({
|
||||
abortMultipartUpload: () => Promise.resolve(),
|
||||
completeMultipartUpload: () => Promise.resolve(),
|
||||
createMultipartUpload: () => Promise.resolve('upload-id'),
|
||||
findMultipartUpload: () => Promise.resolve(null),
|
||||
insertMultipartPart: () => Promise.resolve(),
|
||||
listMultipartParts: () => Promise.resolve([]),
|
||||
listMultipartUploadsByBucket: () =>
|
||||
Promise.resolve({ uploads: [], isTruncated: false, nextKeyMarker: null }),
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/chunked-storage', () => ({
|
||||
createChunkedObjectResponse: () => Promise.resolve(new Response('')),
|
||||
storeFileInTelegramChunks: () => Promise.resolve({ fileHash: 'hash' }),
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/s3/auth', () => ({
|
||||
verifyPresignedUrl: () => Promise.resolve({ isValid: true }),
|
||||
verifySignature: () => Promise.resolve({ isValid: true }),
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/telegram', () => ({
|
||||
forwardToStorage: () =>
|
||||
Promise.resolve({
|
||||
telegramFileId: 'mock-tg-id',
|
||||
telegramFileUniqueId: 'mock-tg-unique',
|
||||
storageMessageId: 12345,
|
||||
}),
|
||||
getFileInfo: () =>
|
||||
Promise.resolve({
|
||||
bot_token: '123456:ABC-DEF',
|
||||
file_path: 'documents/file.txt',
|
||||
file_size: 100,
|
||||
mime_type: 'text/plain',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('S3 bucket configuration compatibility', () => {
|
||||
let handleS3Request: typeof import('../src/routes/s3').handleS3Request;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ handleS3Request } = await import('../src/routes/s3'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('returns VersioningConfiguration for path-style GetBucketVersioning', async () => {
|
||||
const res = await handleS3Request(new Request('http://localhost:3000/gitea?versioning'));
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toContain('application/xml');
|
||||
expect(body).toContain('<VersioningConfiguration');
|
||||
expect(body).not.toContain('<ListBucketResult');
|
||||
});
|
||||
|
||||
it('returns VersioningConfiguration for virtual-hosted GetBucketVersioning', async () => {
|
||||
const res = await handleS3Request(
|
||||
new Request('http://gitea.localhost:3000/?versioning'),
|
||||
'gitea',
|
||||
);
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(body).toContain('<VersioningConfiguration');
|
||||
expect(body).not.toContain('<ListBucketResult');
|
||||
});
|
||||
});
|
||||
+10
-3
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* S3 compatibility E2E tests using the official AWS SDK v3.
|
||||
*
|
||||
* Tests that the TeleUploader S3 gateway is compatible with standard
|
||||
* Tests that the file storage gateway is compatible with standard
|
||||
* AWS SDK clients. All operations are exercised against the production
|
||||
* endpoint.
|
||||
*
|
||||
* Prerequisites (env vars):
|
||||
* - S3_ACCESS_KEY (default: teleuploader-admin)
|
||||
* - S3_ACCESS_KEY (default: filedrop-admin)
|
||||
* - S3_SECRET_KEY (required)
|
||||
* - BASE_URL (default: https://upload.asepharyana.my.id)
|
||||
*
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
DeleteBucketCommand,
|
||||
DeleteObjectCommand,
|
||||
DeleteObjectsCommand,
|
||||
GetBucketVersioningCommand,
|
||||
GetObjectCommand,
|
||||
HeadBucketCommand,
|
||||
HeadObjectCommand,
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
|
||||
// ── Config ───────────────────────────────────────────────────────────────────
|
||||
const BASE_URL = process.env.BASE_URL || 'https://upload.asepharyana.my.id';
|
||||
const S3_KEY = process.env.S3_ACCESS_KEY || 'teleuploader-admin';
|
||||
const S3_KEY = process.env.S3_ACCESS_KEY || 'filedrop-admin';
|
||||
const S3_SECRET = process.env.S3_SECRET_KEY;
|
||||
|
||||
const TS = Date.now().toString(36);
|
||||
@@ -133,6 +134,12 @@ describe('S3 SDK compatibility', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('GetBucketVersioning returns disabled configuration for Gitea compatibility', async () => {
|
||||
const versioning = await s3.send(new GetBucketVersioningCommand({ Bucket: BUCKET }));
|
||||
expect(versioning.Status).toBeUndefined();
|
||||
expect(versioning.MFADelete).toBeUndefined();
|
||||
});
|
||||
|
||||
// ── Object operations ──────────────────────────────────────────────────────
|
||||
|
||||
it('PutObject stores text content', async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('Swagger Documentation Endpoints', () => {
|
||||
paths: Record<string, { get?: object; post?: object }>;
|
||||
};
|
||||
expect(body.openapi).toBe('3.0.0');
|
||||
expect(body.info.title).toBe('TeleUploader API');
|
||||
expect(body.info.title).toBe('FileDrop API');
|
||||
expect(body.paths).toHaveProperty('/health');
|
||||
expect(body.paths).toHaveProperty('/api/upload');
|
||||
expect(body.paths).toHaveProperty('/f/{public_id}');
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ const cleanup = async (...paths: string[]) => {
|
||||
|
||||
describe('ZIP utilities', () => {
|
||||
it('should create a zip and extract entries by name', async () => {
|
||||
const firstPath = `/tmp/teleuploader-test-${crypto.randomUUID()}-1.txt`;
|
||||
const secondPath = `/tmp/teleuploader-test-${crypto.randomUUID()}-2.txt`;
|
||||
const firstPath = `/tmp/filedrop-test-${crypto.randomUUID()}-1.txt`;
|
||||
const secondPath = `/tmp/filedrop-test-${crypto.randomUUID()}-2.txt`;
|
||||
await writeFile(firstPath, 'hello');
|
||||
await writeFile(secondPath, 'world');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user