using System.Text.Json; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; using TCMSBLL.Interfaces; using TCMSDAL.DTO; using TCMSDAL.Interfaces; namespace TCMSBLL.Implementations; /// /// Business logic for test runs and run results. /// Includes Mochawesome JSON ingestion for Cypress CI webhook. /// public class TCMSTestRunBLL : ITCMSTestRunBLL { private readonly ITCMSTestRunDAL _Dal; private readonly ITCMSTestCaseDAL _TestCaseDal; private readonly ITCMSCoverageDAL _CoverageDal; private readonly ILogger _Logger; public TCMSTestRunBLL( ITCMSTestRunDAL dal, ITCMSTestCaseDAL testCaseDal, ITCMSCoverageDAL coverageDal, ILogger logger) { _Dal = dal; _TestCaseDal = testCaseDal; _CoverageDal = coverageDal; _Logger = logger; } public async Task GetTestRunAsync( int testRunId, LoginDTO login, CancellationToken ct) => await _Dal.GetTestRunAsync(testRunId, login, ct).ConfigureAwait(false); public async Task> GetTestRunListAsync( int? projectId, int? moduleId, string? runType, string? status, LoginDTO login, CancellationToken ct) => await _Dal.GetTestRunListAsync(projectId, moduleId, runType, status, login, ct).ConfigureAwait(false); public async Task> GetRunResultsAsync( int testRunId, LoginDTO login, CancellationToken ct) => await _Dal.GetRunResultsAsync(testRunId, login, ct).ConfigureAwait(false); public async Task> GetDashboardAsync( int? projectId, LoginDTO login, CancellationToken ct) => await _Dal.GetDashboardAsync(projectId, login, ct).ConfigureAwait(false); public async Task SaveTestRunAsync( TCMSTestRunDTO dto, LoginDTO login, CancellationToken ct) { try { if (string.IsNullOrWhiteSpace(dto.RunName)) return "Run Name is required."; if (dto.ProjectId == 0) return "Project is required."; bool isNew = dto.TestRunId == 0; if (isNew) { int seq = await _Dal.GetNextRunSequenceAsync(login, ct).ConfigureAwait(false); dto.RunCode = $"RUN-{DateTime.UtcNow.Year}-{seq:D4}"; int newId = await _Dal.SaveTestRunAsync(dto, login, ct).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {newId}"; } else { await _Dal.UpdateTestRunStatusAsync(dto.TestRunId, dto.Status, login.UserId, login, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } } catch (Exception ex) { _Logger.LogError(ex, "SaveTestRun failed for project {ProjectId}", dto.ProjectId); throw; } } public async Task UpdateTestRunStatusAsync( int testRunId, string status, LoginDTO login, CancellationToken ct) { await _Dal.UpdateTestRunStatusAsync(testRunId, status, login.UserId, login, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } public async Task UpdateRunResultAsync( TCMSRunResultDTO dto, LoginDTO login, CancellationToken ct) { try { await _Dal.UpsertRunResultAsync(dto, login, ct).ConfigureAwait(false); // Denormalize last run status on test case await _TestCaseDal.UpdateLastRunStatusAsync(dto.TestCaseId, dto.Result, login, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { _Logger.LogError(ex, "UpdateRunResult failed for TC {TestCaseId}", dto.TestCaseId); throw; } } /// /// Ingests a Cypress Mochawesome JSON report. /// /// Mochawesome structure (simplified): /// { stats: {...}, results: [{ suites: [{ tests: [{ title: "...", pass: bool, err: {...} }] }] }] } /// /// TC-ID extraction: regex /@TC-[A-Z]+-\d+/ in test title. /// Maps each test result → TTCMSRUNRESULT row. /// Creates a coverage snapshot for each affected module after ingestion. /// public async Task IngestCypressReportAsync( int projectId, int moduleId, string mochawesomeJson, LoginDTO login, CancellationToken ct) { try { using var doc = JsonDocument.Parse(mochawesomeJson); var root = doc.RootElement; // Create the test run int seq = await _Dal.GetNextRunSequenceAsync(login, ct).ConfigureAwait(false); var run = new TCMSTestRunDTO { RunCode = $"RUN-{DateTime.UtcNow.Year}-{seq:D4}", RunName = $"Cypress CI — {DateTime.UtcNow:yyyy-MM-dd HH:mm}", ProjectId = projectId, ModuleId = moduleId, RunType = "Cypress", SuiteType = "Regression", Status = "Completed", ActualStart = DateTime.UtcNow }; int runId = await _Dal.SaveTestRunAsync(run, login, ct).ConfigureAwait(false); // Parse results and look for @TC-XXX-NNN tags var tcIdRegex = new System.Text.RegularExpressions.Regex(@"@TC-[A-Z]+-\d+"); int passed = 0, failed = 0, skipped = 0; if (root.TryGetProperty("results", out var results)) { foreach (var result in results.EnumerateArray()) { if (!result.TryGetProperty("suites", out var suites)) continue; foreach (var suite in suites.EnumerateArray()) { if (!suite.TryGetProperty("tests", out var tests)) continue; foreach (var test in tests.EnumerateArray()) { string title = test.TryGetProperty("title", out var t) ? t.GetString() ?? "" : ""; bool isPending = test.TryGetProperty("pending", out var pend) && pend.GetBoolean(); bool isPass = test.TryGetProperty("pass", out var p) && p.GetBoolean(); string resultStr = isPending ? "Skip" : isPass ? "Pass" : "Fail"; string? failReason = null; if (!isPass && !isPending && test.TryGetProperty("err", out var err)) { err.TryGetProperty("message", out var msg); failReason = msg.GetString(); } int? durationMs = test.TryGetProperty("duration", out var dur) ? dur.GetInt32() : null; // Extract TC-ID var match = tcIdRegex.Match(title); if (match.Success) { string tcIdTag = match.Value.TrimStart('@'); // Look up test case ID by TcId var tcList = await _TestCaseDal.GetTestCaseListAsync( moduleId, null, null, null, null, login, ct).ConfigureAwait(false); var tc = tcList.FirstOrDefault(x => x.TcId == tcIdTag); if (tc is not null) { await _Dal.UpsertRunResultAsync(new TCMSRunResultDTO { TestRunId = runId, TestCaseId = tc.TestCaseId, Result = resultStr, FailureReason = failReason, ExecutionMs = durationMs, ExecutedBy = "Cypress CI", TenantId = login.ClientId }, login, ct).ConfigureAwait(false); await _TestCaseDal.UpdateLastRunStatusAsync(tc.TestCaseId, resultStr, login, ct).ConfigureAwait(false); if (resultStr == "Pass") passed++; else if (resultStr == "Fail") failed++; else skipped++; } } } } } } // Mark run as completed await _Dal.UpdateTestRunStatusAsync(runId, "Completed", login.UserId, login, ct).ConfigureAwait(false); // Write coverage snapshot int total = passed + failed + skipped; if (total > 0) { await _CoverageDal.SaveCoverageSnapshotAsync(new TCMSCoverageSnapshotDTO { ModuleId = moduleId, TestRunId = runId, TotalCases = total, PassedCases = passed, CoveragePct = Math.Round((decimal)passed / total * 100, 2), TenantId = login.ClientId }, login, ct).ConfigureAwait(false); } return $"Cypress report ingested. Run: RUN-{DateTime.UtcNow.Year}-{seq:D4}. Pass: {passed} Fail: {failed} Skip: {skipped}"; } catch (JsonException jex) { _Logger.LogError(jex, "IngestCypressReport: invalid JSON"); return "Invalid Mochawesome JSON format."; } catch (Exception ex) { _Logger.LogError(ex, "IngestCypressReport failed for module {ModuleId}", moduleId); throw; } } }