138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
import { describe, expect, test, beforeEach } from "vitest";
|
|
|
|
import { resetApiRuntimeConfigForTests, setApiRuntimeConfigForTests } from "./config.js";
|
|
import { createStorageSignedDownloadUrl, resetStorageCachesForTests, storageCacheStatsForTests } from "./storage.js";
|
|
|
|
const baseConfig = {
|
|
appName: "Test API",
|
|
nodeEnv: "test",
|
|
revision: "unknown",
|
|
listen: { port: 3001 },
|
|
routing: {
|
|
appBaseDomain: "localhost:3000",
|
|
webAppHost: "app.localhost:3000",
|
|
adminAppHost: "admin.localhost:3000",
|
|
apiHost: "api.localhost:3000",
|
|
portalAppHost: "portal.localhost:3000",
|
|
protocol: "http",
|
|
},
|
|
origins: {},
|
|
apiAccessToken: "service-token",
|
|
jwtSecret: "jwt-secret",
|
|
analytics: {},
|
|
mailjet: {},
|
|
lago: { timeoutMs: 10000 },
|
|
mollie: { timeoutMs: 10000 },
|
|
apiKeys: {
|
|
allowLegacyPlaintextFallback: true,
|
|
},
|
|
storage: {
|
|
endpoint: "https://storage.example.com",
|
|
region: "us-east-1",
|
|
accessKeyId: "test-access-key",
|
|
secretAccessKey: "test-secret-key",
|
|
forcePathStyle: "true",
|
|
},
|
|
} as const;
|
|
|
|
describe("createStorageSignedDownloadUrl", () => {
|
|
test("creates a signed GET URL without returning bucket or object key fields", async () => {
|
|
setApiRuntimeConfigForTests(baseConfig);
|
|
|
|
try {
|
|
const result = await createStorageSignedDownloadUrl("exports", {
|
|
key: "tenants/tenant_1/exports/job_1.csv",
|
|
expiresInSeconds: 300,
|
|
responseContentType: "text/csv",
|
|
responseContentDisposition: 'attachment; filename="export.csv"',
|
|
});
|
|
const serialized = JSON.stringify(result);
|
|
|
|
expect(result.method).toBe("GET");
|
|
expect(result.expiresInSeconds).toBe(300);
|
|
expect(result.expiresAt).toBeInstanceOf(Date);
|
|
expect(result.headers).toEqual({});
|
|
expect(result).not.toHaveProperty("bucket");
|
|
expect(result).not.toHaveProperty("key");
|
|
expect(serialized).not.toContain("test-secret-key");
|
|
expect(result.url).toContain("https://storage.example.com/exports/tenants/tenant_1/exports/job_1.csv");
|
|
expect(result.url).toContain("X-Amz-Algorithm=AWS4-HMAC-SHA256");
|
|
expect(result.url).toContain("X-Amz-Signature=");
|
|
expect(result.url).toContain("response-content-type=text%2Fcsv");
|
|
} finally {
|
|
resetApiRuntimeConfigForTests();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("storage adapter + signer caching", () => {
|
|
beforeEach(() => {
|
|
resetStorageCachesForTests();
|
|
});
|
|
|
|
test("cache-hit: repeated calls with the same config reuse the signed-url adapter and signing key", async () => {
|
|
setApiRuntimeConfigForTests(baseConfig);
|
|
|
|
try {
|
|
await createStorageSignedDownloadUrl("exports", {
|
|
key: "a/b/1.csv",
|
|
expiresInSeconds: 300,
|
|
});
|
|
await createStorageSignedDownloadUrl("exports", {
|
|
key: "a/b/2.csv",
|
|
expiresInSeconds: 300,
|
|
});
|
|
await createStorageSignedDownloadUrl("exports", {
|
|
key: "a/b/3.csv",
|
|
expiresInSeconds: 300,
|
|
});
|
|
|
|
const stats = storageCacheStatsForTests();
|
|
expect(stats.signedUrlAdapters).toBe(1);
|
|
// All three calls happen within the same UTC date so the signing key is
|
|
// cached once.
|
|
expect(stats.signingKeys).toBe(1);
|
|
} finally {
|
|
resetApiRuntimeConfigForTests();
|
|
}
|
|
});
|
|
|
|
test("cache-miss: a different bucket produces a new signed-url adapter entry but reuses the per-credential signing key", async () => {
|
|
setApiRuntimeConfigForTests(baseConfig);
|
|
|
|
try {
|
|
await createStorageSignedDownloadUrl("exports", { key: "a.csv", expiresInSeconds: 300 });
|
|
await createStorageSignedDownloadUrl("imports", { key: "b.csv", expiresInSeconds: 300 });
|
|
|
|
const stats = storageCacheStatsForTests();
|
|
expect(stats.signedUrlAdapters).toBe(2);
|
|
expect(stats.signingKeys).toBe(1);
|
|
} finally {
|
|
resetApiRuntimeConfigForTests();
|
|
}
|
|
});
|
|
|
|
test("cache-miss: rotating the secret access key invalidates the cached adapter", async () => {
|
|
setApiRuntimeConfigForTests(baseConfig);
|
|
try {
|
|
await createStorageSignedDownloadUrl("exports", { key: "a.csv", expiresInSeconds: 300 });
|
|
} finally {
|
|
resetApiRuntimeConfigForTests();
|
|
}
|
|
|
|
setApiRuntimeConfigForTests({
|
|
...baseConfig,
|
|
storage: { ...baseConfig.storage, secretAccessKey: "rotated-secret-key" },
|
|
});
|
|
try {
|
|
await createStorageSignedDownloadUrl("exports", { key: "a.csv", expiresInSeconds: 300 });
|
|
|
|
const stats = storageCacheStatsForTests();
|
|
expect(stats.signedUrlAdapters).toBe(2);
|
|
expect(stats.signingKeys).toBe(2);
|
|
} finally {
|
|
resetApiRuntimeConfigForTests();
|
|
}
|
|
});
|
|
});
|