using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using w4c_workflows.Controllers; using w4c_workflows.Data; using w4c_workflows.Models; using Xunit; namespace w4c_workflows.Tests; [Collection("WorkflowsPostgres")] public class RunsControllerTests { private readonly WorkflowsPostgresFixture _fixture; public RunsControllerTests(WorkflowsPostgresFixture fixture) { _fixture = fixture; } private static RunsController Controller(WorkflowsDbContext db, string tenantId) { var controller = new RunsController(db) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }, }; controller.HttpContext.Items["TenantId"] = tenantId; return controller; } private static string Tenant() => "t" + Guid.NewGuid().ToString("N")[..12]; [Fact] public async Task List_returns_runs_newest_first_scoped_to_tenant() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var workflow = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml); var older = WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Succeeded, startedAt: DateTime.UtcNow.AddMinutes(-10), finishedAt: DateTime.UtcNow.AddMinutes(-9)); var newer = WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Failed, startedAt: DateTime.UtcNow.AddMinutes(-1), finishedAt: DateTime.UtcNow, error: "boom"); // A second tenant's runs must never leak into the response. var other = Tenant(); var otherWorkflow = WorkflowDataHelpers.CompileAndSave(db, other, WorkflowDataHelpers.LinearYaml); WorkflowDataHelpers.AddRun(db, otherWorkflow, RunStatus.Running, startedAt: DateTime.UtcNow); var result = Assert.IsType(await Controller(db, tenantId).List(null, 50, 0, default)); var body = Assert.IsType(result.Value); Assert.Equal(2, body.Total); Assert.Equal(new[] { newer.Id, older.Id }, body.Runs.Select(r => r.Id).ToArray()); Assert.Equal("linear", body.Runs[0].WorkflowName); Assert.Equal("boom", body.Runs[0].Error); } [Fact] public async Task List_filters_by_workflow() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var wf1 = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml, "workflows/a.yaml"); var wf2 = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.DurableYaml, "workflows/b.yaml"); WorkflowDataHelpers.AddRun(db, wf1, RunStatus.Succeeded, DateTime.UtcNow); WorkflowDataHelpers.AddRun(db, wf2, RunStatus.Failed, DateTime.UtcNow); var result = Assert.IsType(await Controller(db, tenantId).List(wf1.Id, 50, 0, default)); var body = Assert.IsType(result.Value); Assert.Equal(1, body.Total); Assert.Equal(wf1.Id, body.Runs[0].WorkflowId); } [Fact] public async Task Get_returns_detail_and_404s_for_foreign_run() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var workflow = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml); var run = WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Succeeded, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow, error: null); var controller = Controller(db, tenantId); var result = Assert.IsType(await controller.Get(run.Id, default)); var detail = Assert.IsType(result.Value); Assert.Equal(run.Id, detail.Id); Assert.Equal(workflow.Id, detail.WorkflowId); Assert.Equal("linear", detail.WorkflowName); // A run owned by another tenant is not visible. var other = Tenant(); var otherWorkflow = WorkflowDataHelpers.CompileAndSave(db, other, WorkflowDataHelpers.LinearYaml); var foreignRun = WorkflowDataHelpers.AddRun(db, otherWorkflow, RunStatus.Succeeded, DateTime.UtcNow); Assert.IsType(await controller.Get(foreignRun.Id, default)); } [Fact] public async Task Tasks_returns_task_runs_in_dispatch_order() { var tenantId = Tenant(); await using var db = _fixture.CreateContext(); var workflow = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml); var run = WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Running, DateTime.UtcNow); var root = WorkflowDataHelpers.TaskByKey(workflow, "root"); var a = WorkflowDataHelpers.TaskByKey(workflow, "a"); db.TaskRuns.AddRange( new TaskRun { Id = Guid.NewGuid(), RunId = run.Id, TaskId = root.Id, Attempt = 1, Status = TaskRunStatus.Succeeded, StartedAt = DateTime.UtcNow.AddMinutes(-2), FinishedAt = DateTime.UtcNow.AddMinutes(-1) }, new TaskRun { Id = Guid.NewGuid(), RunId = run.Id, TaskId = a.Id, Attempt = 1, Status = TaskRunStatus.Failed, Error = "boom", IsCompensation = true, StartedAt = DateTime.UtcNow.AddMinutes(-1), FinishedAt = DateTime.UtcNow }); db.SaveChanges(); var result = Assert.IsType(await Controller(db, tenantId).Tasks(run.Id, default)); var tasks = Assert.IsType>(result.Value); Assert.Equal(2, tasks.Count); Assert.Equal("root", tasks[0].TaskKey); Assert.Equal("a", tasks[1].TaskKey); Assert.True(tasks[1].IsCompensation); Assert.Equal("boom", tasks[1].Error); } }