import { INestApplication } from "@nestjs/common"; import { getRepositoryToken } from "@nestjs/typeorm"; import * as http from "http"; import * as fs from "fs/promises"; import { Repository } from "typeorm"; import { buildTestApp } from "./app.harness"; import { FileStorageService } from "../src/file/file-storage.service"; import { FileEntity } from "../src/file/file.entity"; import { ScenarioFileEntity } from "../src/file/scenario-file.entity"; import { ScenarioRunFileEntity } from "../src/file/scenario-run-file.entity"; import { ScenarioEntity } from "../src/scenario/scenario.entity"; import { ScenarioStepEntity } from "../src/scenario/scenario-step.entity"; import { ScenarioRunEntity } from "../src/scenario/scenario-run.entity"; import { ScenarioRunStepEntity } from "../src/scenario/scenario-run-step.entity"; import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service"; describe("Exec Context File Methods Integration Tests", () => { let app: INestApplication; let fileStorageService: FileStorageService; let scheduler: ScenarioSchedulerService; let fileRepo: Repository; let scenarioFileRepo: Repository; let scenarioRunFileRepo: Repository; let scenarioRepo: Repository; let stepRepo: Repository; let runRepo: Repository; let runStepRepo: Repository; let filesDir: string; let testServer: http.Server; let testServerUrl: string; beforeAll(async () => { app = await buildTestApp(); fileStorageService = app.get(FileStorageService); scheduler = app.get(ScenarioSchedulerService); fileRepo = app.get>(getRepositoryToken(FileEntity)); scenarioFileRepo = app.get>( getRepositoryToken(ScenarioFileEntity), ); scenarioRunFileRepo = app.get>( getRepositoryToken(ScenarioRunFileEntity), ); scenarioRepo = app.get>( getRepositoryToken(ScenarioEntity), ); stepRepo = app.get>( getRepositoryToken(ScenarioStepEntity), ); runRepo = app.get>( getRepositoryToken(ScenarioRunEntity), ); runStepRepo = app.get>( getRepositoryToken(ScenarioRunStepEntity), ); filesDir = fileStorageService.getAbsolutePath(""); // Start a test HTTP server for downloadFile tests testServer = await createTestServer(); testServerUrl = `http://localhost:${(testServer.address() as any).port}`; }); afterAll(async () => { // Close the test server if (testServer) { testServer.close(); } // Clean up files directory try { await fs.rm(filesDir, { recursive: true, force: true }); } catch (err) { // Ignore errors if directory doesn't exist } await app.close(); }); // ── helpers ──────────────────────────────────────────────────────────────── function createTestServer(): Promise { return new Promise((resolve) => { const server = http.createServer((req, res) => { if (req.url === "/test-file.bin") { res.writeHead(200, { "Content-Type": "application/octet-stream" }); res.end(Buffer.from("test downloaded file content")); } else if (req.url === "/test.pdf") { res.writeHead(200, { "Content-Type": "application/pdf" }); res.end(Buffer.from("fake pdf content")); } else { res.writeHead(404); res.end(); } }); server.listen(0, "localhost", () => { resolve(server); }); }); } async function createScenario(name = "test-scenario") { const scenario = await scenarioRepo.save( scenarioRepo.create({ name }), ); return scenario; } async function createStep( scenarioId: string, execCode: string, order = 0, ): Promise { const step = await stepRepo.save( stepRepo.create({ scenarioId, order, execCode, }), ); return step; } async function createRun(scenarioId: string) { const run = await runRepo.save( runRepo.create({ scenarioId, status: "pending" }), ); return run; } async function createRunStep( runId: string, scenarioStepId: string, order = 0, ) { const runStep = await runStepRepo.save( runStepRepo.create({ runId, scenarioStepId, order, status: "pending", }), ); return runStep; } async function waitForRunCompletion( runId: string, maxAttempts = 50, ): Promise { for (let i = 0; i < maxAttempts; i++) { // Run scheduler pickup once await scheduler.pickUpPendingRuns(); // Wait a bit for execution await new Promise((resolve) => setTimeout(resolve, 100)); const run = await runRepo.findOneBy({ id: runId }); if (run && (run.status === "pass" || run.status === "fail")) { return run; } } throw new Error(`Run ${runId} did not complete within timeout`); } // ── context.getScenarioFiles() ───────────────────────────────────────────── describe("context.getScenarioFiles()", () => { it("returns scenario files during a run", async () => { const scenario = await createScenario("get-scenario-files-test"); // Upload a file to the scenario const fileBuffer = Buffer.from("scenario file content"); const uploadedFile = await fileStorageService.saveFile( fileBuffer, "test-file.txt", "text/plain", ); // Create scenario file mapping await scenarioFileRepo.save( scenarioFileRepo.create({ scenarioId: scenario.id, fileId: uploadedFile.id, }), ); // Create a step that calls getScenarioFiles const step = await createStep( scenario.id, "const files = await context.getScenarioFiles(); return files;", ); // Create a run and run step const run = await createRun(scenario.id); await createRunStep(run.id, step.id); // Wait for execution const completedRun = await waitForRunCompletion(run.id); // Verify run completed successfully expect(completedRun.status).toBe("pass"); // Get the step run to check output const runStep = await runStepRepo.findOne({ where: { runId: run.id }, relations: ["scenarioStep"], }); expect(runStep).toBeDefined(); expect(runStep!.output).toBeDefined(); const output = runStep!.output ? JSON.parse(runStep!.output) : null; expect(output).toBeDefined(); expect(output.items).toBeDefined(); expect(Array.isArray(output.items)).toBe(true); expect(output.total).toBeGreaterThanOrEqual(1); // Find the uploaded file in the output const foundFile = output.items.find((f: any) => f.id === uploadedFile.id); expect(foundFile).toBeDefined(); expect(foundFile.name).toBe("test-file.txt"); expect(foundFile.mimeType).toBe("text/plain"); expect(foundFile.size).toBe(fileBuffer.length); expect(foundFile.sha256).toBeDefined(); expect(foundFile.path).toBeDefined(); }); it("returns empty list when no files are attached to scenario", async () => { const scenario = await createScenario("no-files-test"); // Create a step that calls getScenarioFiles const step = await createStep( scenario.id, "const files = await context.getScenarioFiles(); return files;", ); // Create a run and run step const run = await createRun(scenario.id); await createRunStep(run.id, step.id); // Wait for execution const completedRun = await waitForRunCompletion(run.id); expect(completedRun.status).toBe("pass"); const runStep = await runStepRepo.findOne({ where: { runId: run.id }, }); const output = runStep!.output ? JSON.parse(runStep!.output) : null; expect(output).toBeDefined(); expect(output.items).toEqual([]); expect(output.total).toBe(0); }); }); // ── context.downloadFile() ──────────────────────────────────────────────── describe("context.downloadFile()", () => { it("downloads a file and creates a run artifact", async () => { const scenario = await createScenario("download-file-test"); const downloadUrl = `${testServerUrl}/test.pdf`; // Create a step that downloads a file const step = await createStep( scenario.id, `const result = await context.downloadFile('${downloadUrl}'); return result;`, ); // Create a run and run step const run = await createRun(scenario.id); await createRunStep(run.id, step.id); // Wait for execution const completedRun = await waitForRunCompletion(run.id); expect(completedRun.status).toBe("pass"); // Get the step run to check output const runStep = await runStepRepo.findOne({ where: { runId: run.id }, relations: ["scenarioStep"], }); const output = runStep!.output ? JSON.parse(runStep!.output) : null; expect(output).toBeDefined(); expect(output.id).toBeDefined(); expect(output.name).toBeDefined(); expect(output.mimeType).toBe("application/pdf"); expect(output.size).toBeGreaterThan(0); expect(output.sha256).toBeDefined(); expect(output.path).toBeDefined(); // Verify the file was created as a run artifact const runFileMapping = await scenarioRunFileRepo.findOne({ where: { runId: run.id, fileId: output.id }, }); expect(runFileMapping).toBeDefined(); // Verify the file exists on disk const file = await fileRepo.findOneBy({ id: output.id }); expect(file).toBeDefined(); const fullPath = fileStorageService.getAbsolutePath(file!.filePath); await fs.access(fullPath); // Verify file content matches what was downloaded const diskContent = await fs.readFile(fullPath); expect(diskContent).toEqual(Buffer.from("fake pdf content")); // Verify the file has an expiresAt date set expect(file!.expiresAt).toBeDefined(); }); it("downloads file with custom filename", async () => { const scenario = await createScenario("download-custom-filename-test"); const downloadUrl = `${testServerUrl}/test-file.bin`; // Create a step that downloads a file with a custom filename const step = await createStep( scenario.id, `const result = await context.downloadFile('${downloadUrl}', { filename: 'custom.dat' }); return result;`, ); // Create a run and run step const run = await createRun(scenario.id); await createRunStep(run.id, step.id); // Wait for execution const completedRun = await waitForRunCompletion(run.id); expect(completedRun.status).toBe("pass"); const runStep = await runStepRepo.findOne({ where: { runId: run.id }, }); const output = runStep!.output ? JSON.parse(runStep!.output) : null; expect(output.name).toBe("custom.dat"); }); it("creates ScenarioRunFileEntity mapping after download", async () => { const scenario = await createScenario("download-mapping-test"); const downloadUrl = `${testServerUrl}/test.pdf`; // Create a step const step = await createStep( scenario.id, `const result = await context.downloadFile('${downloadUrl}'); return result;`, ); // Create a run and run step const run = await createRun(scenario.id); await createRunStep(run.id, step.id); // Wait for execution await waitForRunCompletion(run.id); // Verify that a ScenarioRunFileEntity was created const runFileCount = await scenarioRunFileRepo.count({ where: { runId: run.id }, }); expect(runFileCount).toBeGreaterThanOrEqual(1); // Verify the mapping links to the correct run const mappings = await scenarioRunFileRepo.find({ where: { runId: run.id }, }); expect(mappings.length).toBeGreaterThanOrEqual(1); for (const mapping of mappings) { expect(mapping.runId).toBe(run.id); expect(mapping.fileId).toBeDefined(); } }); it("file returned by downloadFile has correct metadata", async () => { const scenario = await createScenario("download-metadata-test"); const downloadUrl = `${testServerUrl}/test.pdf`; // Create a step const step = await createStep( scenario.id, `const result = await context.downloadFile('${downloadUrl}'); return { hasId: result.id !== undefined, hasName: result.name !== undefined, hasMimeType: result.mimeType !== undefined, hasSize: result.size > 0, hasSha256: result.sha256 !== undefined, hasPath: result.path !== undefined };`, ); // Create a run and run step const run = await createRun(scenario.id); await createRunStep(run.id, step.id); // Wait for execution const completedRun = await waitForRunCompletion(run.id); expect(completedRun.status).toBe("pass"); const runStep = await runStepRepo.findOne({ where: { runId: run.id }, }); const output = runStep!.output ? JSON.parse(runStep!.output) : null; expect(output).toBeDefined(); expect(output.hasId).toBe(true); expect(output.hasName).toBe(true); expect(output.hasMimeType).toBe(true); expect(output.hasSize).toBe(true); expect(output.hasSha256).toBe(true); expect(output.hasPath).toBe(true); }); }); });