Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -327,4 +327,4 @@ coverage-reports/
unit-test-results.xml

# newman
newman
newman
71 changes: 39 additions & 32 deletions devops-pipelines/backend/backend-build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,71 +10,78 @@ trigger:
pr: none

variables:
- group: 'cmrc-deployment-global'
- name: 'buildConfiguration'
value: 'Release'
- name: 'dotNetVersion'
value: '10.x'
- name: 'buildArtifact'
value: 'cmrc-main-api-drop'
- group: "cmrc-deployment-global"
- name: "buildConfiguration"
value: "Release"
- name: "dotNetVersion"
value: "10.x"
- name: "buildArtifact"
value: "cmrc-main-api-drop"

stages:
- stage: Build
displayName: 'Build Main API'
pool: 'CMRC PreProd Pool'
displayName: "Build Main API"
pool: "CMRC PreProd Pool"
jobs:
- job: Build_Main_API
displayName: 'Build Main API'
displayName: "Build Main API"
steps:
- template: ../templates/dotnet-build-steps.yml
parameters:
projectPath: 'src/backend/Cps.CaseManagement.Api/Cps.CaseManagement.Api.csproj'
projectPath: "src/backend/Cps.CaseManagement.Api/Cps.CaseManagement.Api.csproj"
projectName: $(buildArtifact)
buildConfiguration: $(buildConfiguration)
dotNetVersion: $(dotNetVersion)
useLocalNuGet: true
publishOutput: true
runTests: false

- template: ../templates/backend-deploy-stages.yml
parameters:
buildStageName: 'Build'
environment: 'dev'
buildStageName: "Build"
environment: "dev"
requireValidation: false
buildArtifact: $(buildArtifact)

- stage: Run_Postman_Tests_dev
displayName: 'Dev - Run Postman Tests'
dependsOn: 'Deploy_Backend_dev' # final stage of /templates/backend-deploy-stages.yml
pool: 'CMRC PreProd Pool'
displayName: "Dev - Run Postman Tests"
dependsOn: "Deploy_Backend_dev" # final stage of /templates/backend-deploy-stages.yml
pool: "CMRC PreProd Pool"
jobs:
- template: ../templates/postman-test-job.yml
parameters:
appName: 'fa-cmrc-api-dev'
environment: 'dev'
postmanCollectionPath: '$(System.DefaultWorkingDirectory)/src/backend/postman/RegisterCaseAPITests.postman_collection.json'
nodeVersion: '24.x'
- template: ../templates/postman-test-job.yml
parameters:
appName: "fa-cmrc-api-dev"
environment: "dev"
postmanCollectionPath: "$(System.DefaultWorkingDirectory)/src/backend/postman/RegisterCaseAPITests.postman_collection.json"
nodeVersion: "24.x"

- template: ../templates/backend-deploy-stages.yml
parameters:
buildStageName: 'Build'
environment: 'staging'
buildStageName: "Build"
environment: "staging"
requireValidation: true
notifyUsers: $(preprodNotifyUsers)
approvers: $(preprodApprovers)
deployToSlot: true
slotName: 'stg'
slotName: "stg"
buildArtifact: $(buildArtifact)

- stage: Run_E2E_Tests
displayName: "Run E2E tests"
jobs:
- template: ../templates/e2e-playwright-test-job.yml
parameters:
environment: "staging"

- template: ../templates/backend-deploy-stages.yml
parameters:
buildStageName: 'Build'
environment: 'prod'
buildStageName: "Build"
environment: "prod"
requireValidation: true
notifyUsers: $(prodNotifyUsers)
approvers: $(prodApprovers)
agentPool: 'CMRC Prod Pool'
azureSubscription: 'Azure Pipeline: CMRC - Prod'
agentPool: "CMRC Prod Pool"
azureSubscription: "Azure Pipeline: CMRC - Prod"
deployToSlot: true
slotName: 'stg'
buildArtifact: $(buildArtifact)
slotName: "stg"
buildArtifact: $(buildArtifact)
62 changes: 62 additions & 0 deletions devops-pipelines/templates/e2e-playwright-test-job.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
parameters:
- name: environment
type: string
values:
- dev
- staging
- name: agentPool
type: string
default: "CMRC PreProd Pool"

jobs:
- job: runE2ETests
displayName: "Run E2E Tests with Playwright"
pool: ${{ parameters.agentPool }}
variables:
- group: cmrc-automated-testing-${{ parameters.environment }}
- group: cmrc-backend-config-${{ parameters.environment }}
- group: cmrc-ui-config-${{ parameters.environment }}
- name: nodeVersion
value: "24.x"
- name: workingDir
value: "$(System.DefaultWorkingDirectory)/src/ui-spa"

steps:
- task: UseNode@1
displayName: "Use Node.js"
inputs:
version: "$(nodeVersion)"

- task: Npm@1
displayName: "Install NPM Dependencies"
inputs:
command: "ci"
workingDir: "$(workingDir)"

- script: node_modules/.bin/playwright install --with-deps chromium
displayName: "Install Browsers"
workingDirectory: $(workingDir)

- script: npm run e2e:test:ci
workingDirectory: $(workingDir)
displayName: "Run E2E Tests"
env:
E2E_FRONTEND_URL: $(RedirectUrlRegisterCaseUi)
E2E_API_BASE_URL: $(VITE_GATEWAY_BASE_URL)
E2E_AAD_USERNAME: $(AadUserName)
E2E_AAD_PASSWORD: $(AadUserPassword)
E2E_CMS_USERNAME: $(CmsUserName)
E2E_CMS_PASSWORD: $(CmsUserPassword)

- task: PublishTestResults@2
displayName: "Publish Test Results (JUnit)"
inputs:
testResultsFormat: JUnit
testResultsFiles: "$(workingDir)/e2e-tests/e2e-test-results.xml"
publishRunAttachments: false
condition: succeededOrFailed()

- publish: "$(workingDir)/e2e-tests/playwright-report"
artifact: "$(System.JobId)-e2e-playwright-report"
displayName: "Publish Report Artifact"
condition: succeededOrFailed()
23 changes: 15 additions & 8 deletions devops-pipelines/ui/ui-build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,32 @@ variables:
stages:
- template: ../templates/ui-build-and-deploy-stages.yml
parameters:
environment: 'dev'
environment: "dev"
requireValidation: false

- template: ../templates/ui-build-and-deploy-stages.yml
parameters:
environment: 'staging'
environment: "staging"
requireValidation: true
notifyUsers: $(preprodNotifyUsers)
approvers: $(preprodApprovers)
deployToSlot: true
slotName: 'stg'

slotName: "stg"

- stage: Run_E2E_Tests
displayName: "Run E2E tests"
jobs:
- template: ../templates/e2e-playwright-test-job.yml
parameters:
environment: "staging"

- template: ../templates/ui-build-and-deploy-stages.yml
parameters:
environment: 'prod'
environment: "prod"
requireValidation: true
notifyUsers: $(prodNotifyUsers)
approvers: $(prodApprovers)
agentPool: 'CMRC Prod Pool'
azureSubscription: 'Azure Pipeline: CMRC - Prod'
agentPool: "CMRC Prod Pool"
azureSubscription: "Azure Pipeline: CMRC - Prod"
deployToSlot: true
slotName: 'stg'
slotName: "stg"
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,9 @@ public static class CaseRegistrationDefaults
public const int MaxAgeYears = 120;

public const int HearingDateYearsWindow = 2;

/// <summary>
/// Maximum length for the optional frontend journey identifier used in telemetry.
/// </summary>
public const int JourneyIdMaxLength = 100;
}
8 changes: 5 additions & 3 deletions src/backend/Cps.CaseManagement.Api/Functions/RegisterCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class RegisterCase(ILogger<RegisterCase> logger, IMdsService mdsService,
[CmsAuthValuesAuth]
[BearerTokenAuth]
[OpenApiParameter(name: HttpHeaderKeys.CorrelationId, In = Microsoft.OpenApi.Models.ParameterLocation.Header, Required = true, Type = typeof(string), Description = "Correlation identifier for tracking the request.")]
[OpenApiRequestBody(ContentType.ApplicationJson, typeof(CaseRegistrationRequest), Description = "Body containing the NetApp connection to create")]
[OpenApiRequestBody(ContentType.ApplicationJson, typeof(CaseRegistrationRequest), Description = "Body containing the case registration details to create.")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: ContentType.ApplicationJson, bodyType: typeof(CaseRegistrationResponseDto), Description = ApiResponseDescriptions.Success)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.BadRequest, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.BadRequest)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.Unauthorized, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.Unauthorized)]
Expand Down Expand Up @@ -76,7 +76,9 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous,
EventTimestamp = DateTime.UtcNow,
Urn = result.Urn,
CaseId = result.CaseId,
Username = context.Username
Username = context.Username,
JourneyId = caseRegistrationRequest.Value.JourneyId,
AreaOrDivisionText = caseRegistrationRequest.Value.AreaOrDivisionText
});

return new OkObjectResult(result);
Expand All @@ -103,4 +105,4 @@ private static async Task<List<string>> ValidateDefendantsAsync(IEnumerable<Case

return errors;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ public class CaseRegisteredEvent : BaseTelemetryEvent

public string Username { get; set; } = string.Empty;

public string? JourneyId { get; set; }

public string? AreaOrDivisionText { get; set; }

public override (IDictionary<string, string> Properties, IDictionary<string, double?> Metrics) ToTelemetryEventProps()
{
return (
new Dictionary<string, string>
{
["urn"] = Urn,
["caseId"] = CaseId.ToString(),
["username"] = Username
["username"] = Username,
["journeyId"] = JourneyId ?? string.Empty,
["areaOrDivisionText"] = AreaOrDivisionText ?? string.Empty
},
new Dictionary<string, double?>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public CaseRegistrationRequestValidator()
.Matches(CaseRegistrationInputPatterns.Alphanumeric)
.WithMessage($"OicShoulderNumber {CaseRegistrationInputPatterns.AlphanumericMessage}");
this.RuleFor(x => x.OicPoliceUnit).MaximumLength(CaseRegistrationDefaults.ShortTextMaxLength);
this.RuleFor(x => x.JourneyId).MaximumLength(CaseRegistrationDefaults.JourneyIdMaxLength);
this.RuleFor(x => x.AreaOrDivisionText).MaximumLength(CaseRegistrationDefaults.ShortTextMaxLength);

this.RuleFor(x => x.HearingDate)
.GreaterThanOrEqualTo(DateTime.Today.AddYears(-CaseRegistrationDefaults.HearingDateYearsWindow))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,31 @@ public void TrackMetric(string name, double value, IDictionary<string, string>?
_telemetryClient.TrackMetric(name, value, properties);
}

public void TrackPageView(string name)
public void TrackPageView(string name, IDictionary<string, string>? properties = null, IDictionary<string, double>? metrics = null)
{
_telemetryClient.TrackPageView(name);
var pageViewTelemetry = new PageViewTelemetry(name);

if (properties != null)
{
foreach (var property in properties)
{
pageViewTelemetry.Properties[property.Key] = property.Value;
}
}

if (metrics != null)
{
foreach (var metric in metrics)
{
pageViewTelemetry.Metrics[metric.Key] = metric.Value;
}
}

_telemetryClient.TrackPageView(pageViewTelemetry);
}

public void TrackTrace(string message, SeverityLevel severityLevel, IDictionary<string, string>? properties = null)
{
_telemetryClient.TrackTrace(message, severityLevel, properties);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ public class BaseUiTelemetryEvent : BaseTelemetryEvent

public override (IDictionary<string, string> Properties, IDictionary<string, double?> Metrics) ToTelemetryEventProps()
{
var telemetryProperties = Properties.SelectMany(dict => dict)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var telemetryProperties = new Dictionary<string, object>();
if (Properties != null)
{
foreach (var dict in Properties.Where(d => d != null))
{
foreach (var kvp in dict)
{
telemetryProperties[kvp.Key] = kvp.Value;
}
}
}

if (telemetryProperties == null || telemetryProperties.Count == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public interface IAppInsightsTelemetryClient
void TrackEvent(string eventName, IDictionary<string, string>? properties = null, IDictionary<string, double>? metrics = null);
void TrackException(Exception exception, IDictionary<string, string>? properties = null, IDictionary<string, double>? metrics = null);
void TrackMetric(string name, double value, IDictionary<string, string>? properties = null);
void TrackPageView(string name);
void TrackPageView(string name, IDictionary<string, string>? properties = null, IDictionary<string, double>? metrics = null);
void TrackTrace(string message, SeverityLevel severityLevel, IDictionary<string, string>? properties = null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,10 @@ public void TrackPageView(BaseTelemetryEvent telemetryEvent)

var (properties, metrics) = PrepareTelemetryEventProps(telemetryEvent);

if (!properties.ContainsKey(PageNameKey))
if (properties == null || !properties.TryGetValue(PageNameKey, out var pageName) || string.IsNullOrWhiteSpace(pageName))
return;

var pageName = properties?[PageNameKey]?.ToString() ?? string.Empty;

if (string.IsNullOrEmpty(pageName))
return;

_telemetryClient.TrackPageView(pageName);
_telemetryClient.TrackPageView(pageName, properties, metrics);
}

public void TrackTrace(BaseTelemetryEvent telemetryEvent)
Expand Down Expand Up @@ -123,6 +118,9 @@ private void TrackInternalEvent(BaseTelemetryEvent telemetryEvent, bool isFailur
{
var (properties, metrics) = telemetryEvent.ToTelemetryEventProps();

properties ??= new Dictionary<string, string>();
metrics ??= new Dictionary<string, double?>();

var nonNullMetrics = metrics.Where(kvp => kvp.Value.HasValue)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value!.Value);

Expand Down
Loading
Loading