import { postgresAndMinioTest } from "@internal/testcontainers"; import { type IOPacket } from "@trigger.dev/core/v3"; import { type PrismaClient } from "@trigger.dev/database"; import { afterAll, describe, expect, it, vi } from "vitest"; import { env } from "~/env.server"; import { processWaitpointCompletionPacket } from "~/runEngine/concerns/waitpointCompletionPacket.server"; import { ServiceValidationError } from "~/v3/services/common.server"; import { normalizeObjectStoreLogicalKeyPathname } from "~/v3/objectStoreClient.server"; import { assertPacketObjectStoreKeyUnderPrefix, assertSafePacketRelativePath, downloadPacketFromObjectStore, formatStorageUri, generatePresignedRequest, generatePresignedUrl, hasObjectStoreClient, INVALID_PACKET_STORAGE_PATH, jsonPacketPresignFailure, normalizePacketRelativePath, parseStorageUri, resolveSafePacketRelativePath, resolveStoreProtocolForPacketPresign, uploadPacketToObjectStore, } from "~/v3/objectStore.server"; // Extend the timeout for container tests vi.setConfig({ testTimeout: 60_000 }); // Helper to create a test environment async function createTestEnvironment(prisma: PrismaClient) { const suffix = Date.now().toString(36); const org = await prisma.organization.create({ data: { title: `Test Org ${suffix}`, slug: `test-org-${suffix}`, }, }); const project = await prisma.project.create({ data: { name: `Test Project ${suffix}`, slug: `test-project-${suffix}`, externalRef: `proj_test${suffix}`, organizationId: org.id, }, }); const environment = await prisma.runtimeEnvironment.create({ data: { slug: "dev", type: "DEVELOPMENT", organizationId: org.id, projectId: project.id, apiKey: `test_key_${suffix}`, pkApiKey: `test_pk_key_${suffix}`, shortcode: suffix.slice(0, 4), }, include: { project: true, organization: true, }, }); return environment; } // Save original env values for restoration in afterAll const originalEnv = process.env; const originalEnvObj = { OBJECT_STORE_BASE_URL: env.OBJECT_STORE_BASE_URL, OBJECT_STORE_BUCKET: env.OBJECT_STORE_BUCKET, OBJECT_STORE_ACCESS_KEY_ID: env.OBJECT_STORE_ACCESS_KEY_ID, OBJECT_STORE_SECRET_ACCESS_KEY: env.OBJECT_STORE_SECRET_ACCESS_KEY, OBJECT_STORE_REGION: env.OBJECT_STORE_REGION, OBJECT_STORE_DEFAULT_PROTOCOL: env.OBJECT_STORE_DEFAULT_PROTOCOL, TASK_PAYLOAD_OFFLOAD_THRESHOLD: env.TASK_PAYLOAD_OFFLOAD_THRESHOLD, }; describe("Object Storage", () => { describe("URI parsing functions", () => { it("should parse URI with protocol", () => { const result = parseStorageUri("s3://run_abc123/payload.json"); expect(result).toEqual({ protocol: "s3", path: "run_abc123/payload.json", }); }); it("should parse URI with R2 protocol", () => { const result = parseStorageUri("r2://batch_xyz/item_0/payload.json"); expect(result).toEqual({ protocol: "r2", path: "batch_xyz/item_0/payload.json", }); }); it("should parse legacy URI without protocol", () => { const result = parseStorageUri("run_abc123/payload.json"); expect(result).toEqual({ protocol: undefined, path: "run_abc123/payload.json", }); }); it("should format URI with protocol", () => { const result = formatStorageUri("run_abc123/payload.json", "s3"); expect(result).toBe("s3://run_abc123/payload.json"); }); it("should format URI without protocol", () => { const result = formatStorageUri("run_abc123/payload.json"); expect(result).toBe("run_abc123/payload.json"); }); }); describe("assertSafePacketRelativePath", () => { const expectInvalidPath = (path: string) => { try { assertSafePacketRelativePath(path); expect.unreachable("expected path validation to throw"); } catch (error) { expect(error).toBeInstanceOf(ServiceValidationError); expect((error as ServiceValidationError).message).toBe(INVALID_PACKET_STORAGE_PATH); expect((error as ServiceValidationError).status).toBe(400); } }; it.each([ "../file.json", "../../other-env/file.json", "foo/../bar.json", "/absolute/path.json", "foo//bar.json", "foo\\bar.json", ".", "..", ])("rejects unsafe path %s with 400 ServiceValidationError", (path) => { expectInvalidPath(path); }); it.each([ "run/%2e%2e/secret.json", "%2e%2e/secret.json", "%2E%2E/secret.json", "run/%2e./payload.json", "run/%2e%2e", "foo/%2e/bar.json", "foo/%2E/bar.json", ])("rejects percent-encoded traversal %s", (path) => { expectInvalidPath(path); }); it("allows normal packet paths", () => { expect(() => assertSafePacketRelativePath("run_123/payload.json")).not.toThrow(); expect(resolveSafePacketRelativePath("run_123/payload.json")).toBe("run_123/payload.json"); }); it("rejects traversal in protocol-prefixed storage URIs", () => { expect(() => assertSafePacketRelativePath(parseStorageUri("s3://../evil.json").path)).toThrow( ServiceValidationError ); }); }); describe("normalizePacketRelativePath", () => { it("collapses redundant segments in safe paths", () => { expect(normalizePacketRelativePath("run/./payload.json")).toBe("run/payload.json"); }); it("collapses encoded parent segments when used without segment decoding (why decode is required)", () => { expect(normalizePacketRelativePath("run/%2e%2e/secret.json")).toBe("secret.json"); }); }); describe("double-encoded traversal segments", () => { it("does not decode %252e%252e to .. in URL pathname (stays literal; prefix check still holds)", () => { const path = "%252e%252e/secret.json"; expect(() => assertSafePacketRelativePath(path)).not.toThrow(); const key = `packets/proj_ref/dev/${path}`; const normalized = normalizeObjectStoreLogicalKeyPathname(key); expect(normalized).toBe(`/packets/proj_ref/dev/${path}`); expect(() => assertPacketObjectStoreKeyUnderPrefix(key, "packets/proj_ref/dev") ).not.toThrow(); }); }); describe("assertPacketObjectStoreKeyUnderPrefix", () => { const prefix = "packets/proj_ref/dev"; it("accepts keys under the packet prefix after URL normalization", () => { expect(() => assertPacketObjectStoreKeyUnderPrefix(`${prefix}/run_123/payload.json`, prefix) ).not.toThrow(); }); it("rejects keys whose normalized pathname escapes the packet prefix", () => { const traversalKey = `${prefix}/%2e%2e/secret.json`; const normalized = normalizeObjectStoreLogicalKeyPathname(traversalKey); expect(normalized).toBe("/packets/proj_ref/secret.json"); expect(() => assertPacketObjectStoreKeyUnderPrefix(traversalKey, prefix)).toThrow( ServiceValidationError ); }); it("rejects env-level traversal via encoded parent segments", () => { const traversalKey = `${prefix}/%2e%2e/secret.json`; expect(normalizeObjectStoreLogicalKeyPathname(traversalKey)).toBe( "/packets/proj_ref/secret.json" ); expect(() => assertPacketObjectStoreKeyUnderPrefix(traversalKey, prefix)).toThrow( ServiceValidationError ); }); }); describe("normalizeObjectStoreLogicalKeyPathname (Aws4FetchClient behavior)", () => { it("decodes %2e%2e segments into parent directory traversal", () => { const key = "packets/proj_ref/dev/run/%2e%2e/secret.json"; expect(normalizeObjectStoreLogicalKeyPathname(key)).toBe("/packets/proj_ref/dev/secret.json"); }); }); describe("jsonPacketPresignFailure", () => { it("returns 400 for invalid packet path validation failures", async () => { const response = jsonPacketPresignFailure({ success: false, error: INVALID_PACKET_STORAGE_PATH, status: 400, }); expect(response.status).toBe(400); expect(await response.json()).toEqual({ error: INVALID_PACKET_STORAGE_PATH }); }); it("returns 500 for other presign failures", async () => { const response = jsonPacketPresignFailure({ success: false, error: "Object store is not configured for protocol: default", }); expect(response.status).toBe(500); expect(await response.json()).toEqual({ error: "Failed to generate presigned URL: Object store is not configured for protocol: default", }); }); it("prefixes unprefixed internal errors exactly once (no double prefix)", async () => { const response = jsonPacketPresignFailure({ success: false, error: "Access Denied", }); const body = await response.json(); expect(body).toEqual({ error: "Failed to generate presigned URL: Access Denied" }); expect(body.error).not.toMatch(/Failed to generate presigned URL: Failed to generate/); }); }); describe("generatePresignedUrl path validation", () => { it.each([ "../file.json", "../../other-env/file.json", "foo/../bar.json", "/absolute/path.json", "run/%2e%2e/secret.json", "%2e%2e/secret.json", "%2E%2E/secret.json", ])("returns 400 failure for unsafe path %s without calling object store", async (filename) => { const result = await generatePresignedUrl("proj_test", "dev", filename, "PUT"); expect(result.success).toBe(false); if (result.success) throw new Error("expected presign to fail"); expect(result.error).toBe(INVALID_PACKET_STORAGE_PATH); expect(result.status).toBe(400); }); it("allows presign for valid packet paths when object store is not configured", async () => { env.OBJECT_STORE_BASE_URL = undefined; env.OBJECT_STORE_ACCESS_KEY_ID = undefined; env.OBJECT_STORE_SECRET_ACCESS_KEY = undefined; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; const result = await generatePresignedUrl("proj_test", "dev", "run_123/payload.json", "PUT"); expect(result.success).toBe(false); if (result.success) throw new Error("expected presign to fail"); expect(result.error).toContain("Object store is not configured"); expect(result.status).toBeUndefined(); }); }); describe("resolveStoreProtocolForPacketPresign", () => { afterEach(() => { env.OBJECT_STORE_DEFAULT_PROTOCOL = originalEnvObj.OBJECT_STORE_DEFAULT_PROTOCOL; }); it("GET uses legacy default for unprefixed keys even when OBJECT_STORE_DEFAULT_PROTOCOL is set", () => { env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; expect(resolveStoreProtocolForPacketPresign("a/b.json", "GET").storeProtocol).toBeUndefined(); }); it("PUT without forceNoPrefix uses OBJECT_STORE_DEFAULT_PROTOCOL for unprefixed keys", () => { env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; expect(resolveStoreProtocolForPacketPresign("a/b.json", "PUT", false).storeProtocol).toBe( "s3" ); }); it("PUT with forceNoPrefix skips OBJECT_STORE_DEFAULT_PROTOCOL for unprefixed keys", () => { env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; expect( resolveStoreProtocolForPacketPresign("a/b.json", "PUT", true).storeProtocol ).toBeUndefined(); }); it("explicit protocol in key wins for PUT with forceNoPrefix", () => { env.OBJECT_STORE_DEFAULT_PROTOCOL = "r2"; expect(resolveStoreProtocolForPacketPresign("s3://x/y.json", "PUT", true).storeProtocol).toBe( "s3" ); }); }); postgresAndMinioTest( "should upload and download data without protocol (legacy)", async ({ minioConfig, prisma }) => { // Override env directly for the default provider env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; const environment = await createTestEnvironment(prisma); const testData = JSON.stringify({ test: "data", value: 123 }); const filename = "test_run/payload.json"; // Upload const uploadedFilename = await uploadPacketToObjectStore( filename, testData, "application/json", environment as any ); // Should return filename without protocol (legacy) expect(uploadedFilename).toBe(filename); // Download const packet: IOPacket = { data: uploadedFilename, dataType: "application/store", }; const downloadedPacket = await downloadPacketFromObjectStore(packet, environment as any); expect(downloadedPacket.dataType).toBe("application/json"); expect(downloadedPacket.data).toBe(testData); // Cleanup await prisma.runtimeEnvironment.delete({ where: { id: environment.id } }); await prisma.project.delete({ where: { id: environment.projectId } }); await prisma.organization.delete({ where: { id: environment.organizationId } }); } ); postgresAndMinioTest( "should upload and download data with protocol prefix", async ({ minioConfig, prisma }) => { // Named provider — controlled via process.env (read dynamically) process.env.OBJECT_STORE_S3_BASE_URL = minioConfig.baseUrl; process.env.OBJECT_STORE_S3_ACCESS_KEY_ID = minioConfig.accessKeyId; process.env.OBJECT_STORE_S3_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; process.env.OBJECT_STORE_S3_REGION = minioConfig.region; process.env.OBJECT_STORE_S3_SERVICE = "s3"; const environment = await createTestEnvironment(prisma); const testData = JSON.stringify({ test: "protocol-data", value: 456 }); const filename = "test_run2/payload.json"; // Upload with explicit protocol — bypasses env.OBJECT_STORE_DEFAULT_PROTOCOL const uploadedFilename = await uploadPacketToObjectStore( filename, testData, "application/json", environment as any, "s3" ); // Should return filename with s3:// protocol expect(uploadedFilename).toBe("s3://test_run2/payload.json"); // Download const packet: IOPacket = { data: uploadedFilename, dataType: "application/store", }; const downloadedPacket = await downloadPacketFromObjectStore(packet, environment as any); expect(downloadedPacket.dataType).toBe("application/json"); expect(downloadedPacket.data).toBe(testData); // Cleanup await prisma.runtimeEnvironment.delete({ where: { id: environment.id } }); await prisma.project.delete({ where: { id: environment.projectId } }); await prisma.organization.delete({ where: { id: environment.organizationId } }); } ); postgresAndMinioTest( "should support migration from default provider to named provider", async ({ minioConfig, prisma }) => { const environment = await createTestEnvironment(prisma); // Step 1: Upload old data without protocol (using default provider) env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; const oldData = JSON.stringify({ legacy: true }); const oldFilename = "old_run/payload.json"; const uploadedOldFilename = await uploadPacketToObjectStore( oldFilename, oldData, "application/json", environment as any ); expect(uploadedOldFilename).toBe(oldFilename); // No protocol // Step 2: Configure new provider (S3) and set default protocol process.env.OBJECT_STORE_S3_BASE_URL = minioConfig.baseUrl; // Same MinIO for testing process.env.OBJECT_STORE_S3_ACCESS_KEY_ID = minioConfig.accessKeyId; process.env.OBJECT_STORE_S3_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; process.env.OBJECT_STORE_S3_REGION = minioConfig.region; process.env.OBJECT_STORE_S3_SERVICE = "s3"; // Step 3: Upload new data with explicit protocol const newData = JSON.stringify({ new: true }); const newFilename = "new_run/payload.json"; const uploadedNewFilename = await uploadPacketToObjectStore( newFilename, newData, "application/json", environment as any, "s3" ); expect(uploadedNewFilename).toBe("s3://new_run/payload.json"); // Has protocol // Step 4: Verify both can be downloaded // Old data (no protocol, uses default provider) const oldPacket: IOPacket = { data: uploadedOldFilename, dataType: "application/store", }; const downloadedOld = await downloadPacketFromObjectStore(oldPacket, environment as any); expect(downloadedOld.data).toBe(oldData); // New data (with protocol, uses named provider) const newPacket: IOPacket = { data: uploadedNewFilename, dataType: "application/store", }; const downloadedNew = await downloadPacketFromObjectStore(newPacket, environment as any); expect(downloadedNew.data).toBe(newData); // Cleanup await prisma.runtimeEnvironment.delete({ where: { id: environment.id } }); await prisma.project.delete({ where: { id: environment.projectId } }); await prisma.organization.delete({ where: { id: environment.organizationId } }); } ); postgresAndMinioTest( "should upload and download using IAM credential chain (AWS SDK path)", async ({ minioConfig, prisma }) => { // IAM mode: override env with bucket but no access keys. // We put the credentials in AWS_* env vars so the S3Client credential // chain picks them up (same as it would from an ECS task role in production). env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_BUCKET = "packets"; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_ACCESS_KEY_ID = undefined; env.OBJECT_STORE_SECRET_ACCESS_KEY = undefined; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; process.env.AWS_ACCESS_KEY_ID = minioConfig.accessKeyId; process.env.AWS_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; process.env.AWS_REGION = minioConfig.region; const environment = await createTestEnvironment(prisma); const testData = JSON.stringify({ iam: true, value: 789 }); const filename = "iam_test_run/payload.json"; const uploadedFilename = await uploadPacketToObjectStore( filename, testData, "application/json", environment as any ); expect(uploadedFilename).toBe(filename); const packet: IOPacket = { data: uploadedFilename, dataType: "application/store", }; const downloadedPacket = await downloadPacketFromObjectStore(packet, environment as any); expect(downloadedPacket.dataType).toBe("application/json"); expect(downloadedPacket.data).toBe(testData); // Cleanup delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_SECRET_ACCESS_KEY; delete process.env.AWS_REGION; await prisma.runtimeEnvironment.delete({ where: { id: environment.id } }); await prisma.project.delete({ where: { id: environment.projectId } }); await prisma.organization.delete({ where: { id: environment.organizationId } }); } ); postgresAndMinioTest( "processWaitpointCompletionPacket offloads above threshold and round-trips download", async ({ minioConfig, prisma }) => { env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; const savedThreshold = env.TASK_PAYLOAD_OFFLOAD_THRESHOLD; env.TASK_PAYLOAD_OFFLOAD_THRESHOLD = 256; const environment = await createTestEnvironment(prisma); const pathPrefix = `waitpoint_completiontest/token`; try { const smallPacket: IOPacket = { data: JSON.stringify({ ok: true }), dataType: "application/json", }; const smallResult = await processWaitpointCompletionPacket( smallPacket, environment as any, pathPrefix ); expect(smallResult).toEqual(smallPacket); const largeBody = "x".repeat(400); const largePacket: IOPacket = { data: largeBody, dataType: "text/plain", }; const largeResult = await processWaitpointCompletionPacket( largePacket, environment as any, pathPrefix ); expect(largeResult.dataType).toBe("application/store"); expect(largeResult.data).toBe(`${pathPrefix}.txt`); const downloadedPacket = await downloadPacketFromObjectStore( { data: largeResult.data, dataType: "application/store", }, environment as any ); expect(downloadedPacket.data).toBe(largeBody); } finally { env.TASK_PAYLOAD_OFFLOAD_THRESHOLD = savedThreshold; await prisma.runtimeEnvironment.delete({ where: { id: environment.id } }); await prisma.project.delete({ where: { id: environment.projectId } }); await prisma.organization.delete({ where: { id: environment.organizationId } }); } } ); describe("hasObjectStoreClient", () => { it("returns false when no store is configured", () => { env.OBJECT_STORE_BASE_URL = undefined; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; delete process.env.OBJECT_STORE_NOTCONFIGURED_BASE_URL; expect(hasObjectStoreClient()).toBe(false); }); it("returns true when default provider base URL is set", () => { env.OBJECT_STORE_BASE_URL = "http://localhost:9000"; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; expect(hasObjectStoreClient()).toBe(true); env.OBJECT_STORE_BASE_URL = undefined; }); it("returns true when named protocol base URL is set", () => { env.OBJECT_STORE_BASE_URL = undefined; env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; process.env.OBJECT_STORE_S3_BASE_URL = "http://localhost:9000"; expect(hasObjectStoreClient()).toBe(true); env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; delete process.env.OBJECT_STORE_S3_BASE_URL; }); }); postgresAndMinioTest( "generatePresignedUrl - PUT then GET round-trip (static credentials / aws4fetch path)", async ({ minioConfig }) => { env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; const projectRef = "proj_presign_test"; const envSlug = "dev"; const filename = "presigned-static/payload.json"; const data = JSON.stringify({ presigned: "static" }); // Upload via presigned PUT const putResult = await generatePresignedUrl(projectRef, envSlug, filename, "PUT"); expect(putResult.success).toBe(true); if (!putResult.success) throw new Error(putResult.error); expect(putResult.storagePath).toBe(filename); const putResponse = await fetch(putResult.url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: data, }); expect(putResponse.ok).toBe(true); // Download via presigned GET const getResult = await generatePresignedUrl(projectRef, envSlug, filename, "GET"); expect(getResult.success).toBe(true); if (!getResult.success) throw new Error(getResult.error); expect(getResult.storagePath).toBeUndefined(); const getResponse = await fetch(getResult.url); expect(getResponse.ok).toBe(true); expect(await getResponse.text()).toBe(data); } ); postgresAndMinioTest( "generatePresignedUrl - PUT unprefixed with OBJECT_STORE_DEFAULT_PROTOCOL returns s3 storagePath", async ({ minioConfig }) => { env.OBJECT_STORE_BASE_URL = undefined; env.OBJECT_STORE_ACCESS_KEY_ID = undefined; env.OBJECT_STORE_SECRET_ACCESS_KEY = undefined; env.OBJECT_STORE_REGION = undefined; process.env.OBJECT_STORE_S3_BASE_URL = minioConfig.baseUrl; process.env.OBJECT_STORE_S3_ACCESS_KEY_ID = minioConfig.accessKeyId; process.env.OBJECT_STORE_S3_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; process.env.OBJECT_STORE_S3_REGION = minioConfig.region; process.env.OBJECT_STORE_S3_SERVICE = "s3"; env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; const projectRef = "proj_presign_v2_style"; const envSlug = "dev"; const filename = "v2-style/payload.json"; const data = JSON.stringify({ v2: true }); const putResult = await generatePresignedUrl(projectRef, envSlug, filename, "PUT"); expect(putResult.success).toBe(true); if (!putResult.success) throw new Error(putResult.error); expect(putResult.storagePath).toBe(`s3://${filename}`); const putResponse = await fetch(putResult.url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: data, }); expect(putResponse.ok).toBe(true); const getResult = await generatePresignedUrl( projectRef, envSlug, putResult.storagePath!, "GET" ); expect(getResult.success).toBe(true); if (!getResult.success) throw new Error(getResult.error); const getResponse = await fetch(getResult.url); expect(getResponse.ok).toBe(true); expect(await getResponse.text()).toBe(data); delete process.env.OBJECT_STORE_S3_BASE_URL; delete process.env.OBJECT_STORE_S3_ACCESS_KEY_ID; delete process.env.OBJECT_STORE_S3_SECRET_ACCESS_KEY; delete process.env.OBJECT_STORE_S3_REGION; delete process.env.OBJECT_STORE_S3_SERVICE; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; } ); postgresAndMinioTest( "generatePresignedUrl - forceNoPrefix PUT fails without legacy default when only S3 named", async ({ minioConfig }) => { env.OBJECT_STORE_BASE_URL = undefined; env.OBJECT_STORE_ACCESS_KEY_ID = undefined; env.OBJECT_STORE_SECRET_ACCESS_KEY = undefined; env.OBJECT_STORE_REGION = undefined; process.env.OBJECT_STORE_S3_BASE_URL = minioConfig.baseUrl; process.env.OBJECT_STORE_S3_ACCESS_KEY_ID = minioConfig.accessKeyId; process.env.OBJECT_STORE_S3_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; process.env.OBJECT_STORE_S3_REGION = minioConfig.region; process.env.OBJECT_STORE_S3_SERVICE = "s3"; env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; const putLegacy = await generatePresignedUrl( "proj_force_noprefix", "dev", "only-legacy/payload.json", "PUT", { forceNoPrefix: true } ); expect(putLegacy.success).toBe(false); const getUnprefixed = await generatePresignedUrl( "proj_force_noprefix", "dev", "any.json", "GET" ); expect(getUnprefixed.success).toBe(false); delete process.env.OBJECT_STORE_S3_BASE_URL; delete process.env.OBJECT_STORE_S3_ACCESS_KEY_ID; delete process.env.OBJECT_STORE_S3_SECRET_ACCESS_KEY; delete process.env.OBJECT_STORE_S3_REGION; delete process.env.OBJECT_STORE_S3_SERVICE; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; } ); postgresAndMinioTest( "generatePresignedUrl - PUT then GET round-trip (IAM credential chain / AWS SDK path)", async ({ minioConfig }) => { env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_BUCKET = "packets"; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_ACCESS_KEY_ID = undefined; env.OBJECT_STORE_SECRET_ACCESS_KEY = undefined; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; process.env.AWS_ACCESS_KEY_ID = minioConfig.accessKeyId; process.env.AWS_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; process.env.AWS_REGION = minioConfig.region; const projectRef = "proj_presign_iam"; const envSlug = "dev"; const filename = "presigned-iam/payload.json"; const data = JSON.stringify({ presigned: "iam" }); // Upload via presigned PUT const putResult = await generatePresignedUrl(projectRef, envSlug, filename, "PUT"); expect(putResult.success).toBe(true); if (!putResult.success) throw new Error(putResult.error); expect(putResult.storagePath).toBe(filename); const putResponse = await fetch(putResult.url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: data, }); expect(putResponse.ok).toBe(true); // Download via presigned GET const getResult = await generatePresignedUrl(projectRef, envSlug, filename, "GET"); expect(getResult.success).toBe(true); if (!getResult.success) throw new Error(getResult.error); const getResponse = await fetch(getResult.url); expect(getResponse.ok).toBe(true); expect(await getResponse.text()).toBe(data); delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_SECRET_ACCESS_KEY; delete process.env.AWS_REGION; } ); postgresAndMinioTest( "generatePresignedRequest - returns a signed Request object", async ({ minioConfig }) => { env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; env.OBJECT_STORE_REGION = minioConfig.region; env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; const result = await generatePresignedRequest( "proj_req_test", "dev", "req-test/file.json", "GET" ); expect(result.success).toBe(true); if (!result.success) throw new Error(result.error); // URL should point at the right key and contain SigV4 query params expect(result.request.url).toContain("packets/proj_req_test/dev/req-test/file.json"); expect(result.request.url).toContain("X-Amz-"); expect(result.request.method).toBe("GET"); } ); // Restore env after all tests afterAll(() => { process.env = originalEnv; env.OBJECT_STORE_BASE_URL = originalEnvObj.OBJECT_STORE_BASE_URL; env.OBJECT_STORE_BUCKET = originalEnvObj.OBJECT_STORE_BUCKET; env.OBJECT_STORE_ACCESS_KEY_ID = originalEnvObj.OBJECT_STORE_ACCESS_KEY_ID; env.OBJECT_STORE_SECRET_ACCESS_KEY = originalEnvObj.OBJECT_STORE_SECRET_ACCESS_KEY; env.OBJECT_STORE_REGION = originalEnvObj.OBJECT_STORE_REGION; env.OBJECT_STORE_DEFAULT_PROTOCOL = originalEnvObj.OBJECT_STORE_DEFAULT_PROTOCOL; env.TASK_PAYLOAD_OFFLOAD_THRESHOLD = originalEnvObj.TASK_PAYLOAD_OFFLOAD_THRESHOLD; }); });