using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TermRemoteCtl.Agent.Sessions; namespace TermRemoteCtl.Agent.IntegrationTests; public sealed class ProjectApiTests { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); [Fact] public async Task Create_List_Update_And_Detail_Project_Flow_Works() { await using var fixture = new AgentFixture(); using var client = fixture.CreateClient(); var createResponse = await client.PostAsJsonAsync( "/api/projects", new { name = "TermRemoteCtl", workingDirectory = fixture.ValidProjectPath }); createResponse.EnsureSuccessStatusCode(); var created = await createResponse.Content.ReadFromJsonAsync(JsonOptions); Assert.NotNull(created); Assert.Equal("TermRemoteCtl", created!.Name); Assert.Equal(fixture.ValidProjectPath, created.WorkingDirectory); var listed = await client.GetFromJsonAsync>("/api/projects", JsonOptions); Assert.NotNull(listed); Assert.Contains(listed!, project => project.ProjectId == created.ProjectId); var registry = fixture.Services.GetRequiredService(); registry.Create( "TermRemoteCtl", DateTimeOffset.UtcNow, projectId: created.ProjectId, workingDirectory: created.WorkingDirectory); var detail = await client.GetFromJsonAsync( $"/api/projects/{created.ProjectId}", JsonOptions); Assert.NotNull(detail); Assert.Equal(created.ProjectId, detail!.ProjectId); Assert.Single(detail.RecentSessions); var updateResponse = await client.PutAsJsonAsync( $"/api/projects/{created.ProjectId}", new { name = "TRC", workingDirectory = fixture.UpdatedProjectPath }); updateResponse.EnsureSuccessStatusCode(); var updated = await updateResponse.Content.ReadFromJsonAsync(JsonOptions); Assert.NotNull(updated); Assert.Equal("TRC", updated!.Name); Assert.Equal(fixture.UpdatedProjectPath, updated.WorkingDirectory); } [Fact] public async Task Create_Project_Returns_BadRequest_For_Invalid_WorkingDirectory() { await using var fixture = new AgentFixture(); using var client = fixture.CreateClient(); var response = await client.PostAsJsonAsync( "/api/projects", new { name = "Broken", workingDirectory = "Z:\\path\\does-not-exist" }); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Delete_Project_Cascades_Associated_Sessions() { await using var fixture = new AgentFixture(); using var client = fixture.CreateClient(); var createProjectResponse = await client.PostAsJsonAsync( "/api/projects", new { name = "TermRemoteCtl", workingDirectory = fixture.ValidProjectPath }); createProjectResponse.EnsureSuccessStatusCode(); var project = await createProjectResponse.Content.ReadFromJsonAsync(JsonOptions); Assert.NotNull(project); var createSessionResponse = await client.PostAsJsonAsync( "/api/sessions", new { projectId = project!.ProjectId }); createSessionResponse.EnsureSuccessStatusCode(); var session = await createSessionResponse.Content.ReadFromJsonAsync(JsonOptions); Assert.NotNull(session); var deleteResponse = await client.DeleteAsync($"/api/projects/{project.ProjectId}"); deleteResponse.EnsureSuccessStatusCode(); var listedProjects = await client.GetFromJsonAsync>("/api/projects", JsonOptions); Assert.NotNull(listedProjects); Assert.DoesNotContain(listedProjects!, item => item.ProjectId == project.ProjectId); var listedSessions = await client.GetFromJsonAsync>("/api/sessions", JsonOptions); Assert.NotNull(listedSessions); Assert.DoesNotContain(listedSessions!, item => item.SessionId == session!.SessionId); } private sealed class AgentFixture : WebApplicationFactory { private readonly string _dataRoot = Path.Combine(Path.GetTempPath(), "TermRemoteCtl.Tests", Guid.NewGuid().ToString("N")); public string ValidProjectPath => Path.Combine(_dataRoot, "project-a"); public string UpdatedProjectPath => Path.Combine(_dataRoot, "project-b"); protected override void ConfigureWebHost(IWebHostBuilder builder) { Directory.CreateDirectory(ValidProjectPath); Directory.CreateDirectory(UpdatedProjectPath); builder.UseEnvironment("Development"); builder.ConfigureAppConfiguration((_, configBuilder) => { configBuilder.AddInMemoryCollection(new Dictionary { ["Agent:DataRoot"] = _dataRoot, ["Agent:BindAddress"] = "127.0.0.1", ["Agent:HttpsPort"] = "9443", ["Agent:WebSocketFrameFlushMilliseconds"] = "33", ["Agent:RingBufferLineLimit"] = "4000" }); }); } public new async ValueTask DisposeAsync() { await base.DisposeAsync(); if (Directory.Exists(_dataRoot)) { Directory.Delete(_dataRoot, true); } } } private sealed record ProjectResponse( string ProjectId, string Name, string WorkingDirectory, DateTimeOffset CreatedAtUtc, DateTimeOffset UpdatedAtUtc); private sealed record ProjectDetailResponse( string ProjectId, string Name, string WorkingDirectory, IReadOnlyList RecentSessions); private sealed record SessionSummaryResponse(string SessionId, string Name, string Status); private sealed record SessionResponse( string SessionId, string Name, string Status, string? ProjectId, DateTimeOffset CreatedAtUtc, DateTimeOffset UpdatedAtUtc); }