Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/api/src/storage.test.ts
T

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