From fa818168fa97f8f98ed22c35e08699dea3698a53 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Mon, 17 Nov 2025 13:37:01 +0100
Subject: [PATCH 01/17] Add process lock middleware
---
.../Middleware/EnableProcessLockAttribute.cs | 4 +
.../Middleware/ProcessLockMiddleware.cs | 120 ++++++
.../Middleware/ProcessLockOptions.cs | 6 +
.../Extensions/HttpClientExtension.cs | 35 ++
.../Telemetry/Telemetry.ProcessLockClient.cs | 26 ++
.../Clients/Storage/ProcessLockClient.cs | 99 +++++
.../Process/ProcessLock/ProcessLockRequest.cs | 6 +
.../ProcessLock/ProcessLockResponse.cs | 6 +
.../Altinn.App.Api.Tests.csproj | 1 +
.../Conventions/EnumSerializationTests.cs | 7 +-
.../Middleware/ProcessLockMiddlewareTests.cs | 388 ++++++++++++++++++
11 files changed, 696 insertions(+), 2 deletions(-)
create mode 100644 src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs
create mode 100644 src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs
create mode 100644 src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs
create mode 100644 src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
create mode 100644 src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs b/src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs
new file mode 100644
index 0000000000..669148f47d
--- /dev/null
+++ b/src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs
@@ -0,0 +1,4 @@
+namespace Altinn.App.Api.Infrastructure.Middleware;
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
+internal sealed class EnableProcessLockAttribute : Attribute;
diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs
new file mode 100644
index 0000000000..a5bc61b81e
--- /dev/null
+++ b/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs
@@ -0,0 +1,120 @@
+using Altinn.App.Core.Helpers;
+using Altinn.App.Core.Infrastructure.Clients.Storage;
+using Microsoft.Extensions.Options;
+
+namespace Altinn.App.Api.Infrastructure.Middleware;
+
+///
+/// Middleware that ensures only one request can proceed at a time by acquiring a lock.
+/// Only applies to endpoints decorated with .
+///
+internal sealed partial class ProcessLockMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ private readonly ProcessLockClient _processClient;
+ private readonly ProcessLockOptions _options;
+
+ public ProcessLockMiddleware(
+ RequestDelegate next,
+ ILogger logger,
+ IOptions options,
+ ProcessLockClient processClient
+ )
+ {
+ _next = next;
+ _logger = logger;
+ _processClient = processClient;
+ _options = options.Value;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ var endpoint = context.GetEndpoint();
+
+ if (endpoint?.Metadata.GetMetadata() is null)
+ {
+ await _next(context);
+ return;
+ }
+
+ var (instanceOwnerPartyId, instanceGuid) =
+ GetInstanceIdentifiers(context)
+ ?? throw new InvalidOperationException("Unable to extract instance identifiers.");
+ Guid? lockId = null;
+
+ try
+ {
+ try
+ {
+ lockId = await _processClient.AcquireProcessLock(
+ instanceGuid,
+ instanceOwnerPartyId,
+ _options.Expiration
+ );
+
+ LogLockAcquired(_logger, lockId.Value);
+ }
+ catch (PlatformHttpException e)
+ {
+ LogLockAcquisitionFailed(_logger);
+ var problem = TypedResults.Problem(
+ detail: e.Message,
+ statusCode: e.Response.IsSuccessStatusCode ? 500 : (int)e.Response.StatusCode,
+ title: "Failed to acquire lock."
+ );
+
+ await problem.ExecuteAsync(context);
+
+ return;
+ }
+
+ await _next(context);
+ }
+ finally
+ {
+ if (lockId is not null)
+ {
+ try
+ {
+ await _processClient.ReleaseProcessLock(instanceGuid, instanceOwnerPartyId, lockId.Value);
+
+ LogLockReleased(_logger, lockId.Value);
+ }
+ catch (Exception e)
+ {
+ LogLockReleaseFailed(_logger, lockId.Value, e);
+ }
+ }
+ }
+ }
+
+ private static (int instanceOwnerPartyId, Guid instanceGuid)? GetInstanceIdentifiers(HttpContext context)
+ {
+ var routeData = context.GetRouteData();
+
+ if (
+ routeData.Values.TryGetValue("instanceOwnerPartyId", out var partyIdObj)
+ && routeData.Values.TryGetValue("instanceGuid", out var guidObj)
+ && int.TryParse(partyIdObj?.ToString(), out var partyId)
+ && Guid.TryParse(guidObj?.ToString(), out var guid)
+ )
+ {
+ return (partyId, guid);
+ }
+
+ return null;
+ }
+
+ [LoggerMessage(1, LogLevel.Debug, "Failed to acquire process lock.")]
+ private static partial void LogLockAcquisitionFailed(ILogger logger);
+
+ [LoggerMessage(2, LogLevel.Debug, "Acquired process lock with id: {LockId}")]
+ private static partial void LogLockAcquired(ILogger logger, Guid lockId);
+
+ [LoggerMessage(3, LogLevel.Debug, "Released process lock with id: {LockId}")]
+ private static partial void LogLockReleased(ILogger logger, Guid lockId);
+
+ [LoggerMessage(4, LogLevel.Error, "Failed to release process lock with id: {LockId}")]
+ private static partial void LogLockReleaseFailed(ILogger logger, Guid lockId, Exception e);
+}
diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs b/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs
new file mode 100644
index 0000000000..94b9d3a218
--- /dev/null
+++ b/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs
@@ -0,0 +1,6 @@
+namespace Altinn.App.Api.Infrastructure.Middleware;
+
+internal sealed class ProcessLockOptions
+{
+ public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5);
+}
diff --git a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs
index d6ff3aeca1..16d5e8a1ef 100644
--- a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs
+++ b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs
@@ -144,6 +144,41 @@ public static async Task GetStreamingAsync(
return await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
}
+ ///
+ /// Extension that add authorization header to request
+ ///
+ /// The HttpClient
+ /// the authorization token (jwt)
+ /// The request Uri
+ /// The http content
+ /// The platformAccess tokens
+ /// The cancellation token
+ /// A HttpResponseMessage
+ public static async Task PatchAsync(
+ this HttpClient httpClient,
+ string authorizationToken,
+ string requestUri,
+ HttpContent? content,
+ string? platformAccessToken = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ using HttpRequestMessage request = new(HttpMethod.Patch, requestUri);
+ request.Content = content;
+
+ request.Headers.Authorization = new AuthenticationHeaderValue(
+ Constants.AuthorizationSchemes.Bearer,
+ authorizationToken
+ );
+
+ if (!string.IsNullOrEmpty(platformAccessToken))
+ {
+ request.Headers.Add(Constants.General.PlatformAccessTokenHeaderName, platformAccessToken);
+ }
+
+ return await httpClient.SendAsync(request, cancellationToken);
+ }
+
///
/// Extension that add authorization header to request
///
diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
new file mode 100644
index 0000000000..eda8e56812
--- /dev/null
+++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+
+namespace Altinn.App.Core.Features;
+
+partial class Telemetry
+{
+ internal Activity? StartAqzActivity() => ActivitySource.StartActivity("ProcessClient.GetProcessDefinition");
+
+ internal Activity? StartAcquireProcessLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
+ {
+ var activity = ActivitySource.StartActivity("AcquireProcessLock");
+ activity?.SetInstanceId(instanceGuid);
+ activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId);
+
+ return activity;
+ }
+
+ internal Activity? StartReleaseProcessLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
+ {
+ var activity = ActivitySource.StartActivity("ReleaseProcessLock");
+ activity?.SetInstanceId(instanceGuid);
+ activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId);
+
+ return activity;
+ }
+}
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
new file mode 100644
index 0000000000..c3a21b2ed2
--- /dev/null
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
@@ -0,0 +1,99 @@
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using Altinn.App.Core.Configuration;
+using Altinn.App.Core.Constants;
+using Altinn.App.Core.Extensions;
+using Altinn.App.Core.Features;
+using Altinn.App.Core.Helpers;
+using Altinn.App.Core.Internal.Process.ProcessLock;
+using AltinnCore.Authentication.Utils;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+
+namespace Altinn.App.Core.Infrastructure.Clients.Storage;
+
+internal sealed class ProcessLockClient
+{
+ private readonly AppSettings _appSettings;
+ private readonly HttpClient _client;
+ private readonly Telemetry? _telemetry;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public ProcessLockClient(
+ IOptions platformSettings,
+ IOptions appSettings,
+ IHttpContextAccessor httpContextAccessor,
+ HttpClient httpClient,
+ Telemetry? telemetry = null
+ )
+ {
+ _appSettings = appSettings.Value;
+ _httpContextAccessor = httpContextAccessor;
+ httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint);
+ httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey);
+ httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ _client = httpClient;
+ _telemetry = telemetry;
+ }
+
+ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerPartyId, TimeSpan expiration)
+ {
+ using var activity = _telemetry?.StartAcquireProcessLockActivity(instanceGuid, instanceOwnerPartyId);
+ string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/lock";
+ string token = JwtTokenUtil.GetTokenFromContext(
+ _httpContextAccessor.HttpContext,
+ _appSettings.RuntimeCookieName
+ );
+
+ var request = new ProcessLockRequest { Expiration = (int)expiration.TotalSeconds };
+ var content = JsonContent.Create(request);
+
+ HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw await PlatformHttpException.CreateAsync(response);
+ }
+
+ Guid? lockId = null;
+ try
+ {
+ var lockResponse = await response.Content.ReadFromJsonAsync();
+ lockId = lockResponse?.LockId;
+ }
+ catch
+ {
+ // Throw exception below
+ }
+
+ if (lockId is null || lockId.Value == Guid.Empty)
+ {
+ throw new PlatformHttpException(
+ response,
+ "The response from the lock acquisition endpoint was not expected."
+ );
+ }
+
+ return lockId.Value;
+ }
+
+ public async Task ReleaseProcessLock(Guid instanceGuid, int instanceOwnerPartyId, Guid lockId)
+ {
+ using var activity = _telemetry?.StartReleaseProcessLockActivity(instanceGuid, instanceOwnerPartyId);
+ string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/lock/{lockId}";
+ string token = JwtTokenUtil.GetTokenFromContext(
+ _httpContextAccessor.HttpContext,
+ _appSettings.RuntimeCookieName
+ );
+
+ var request = new ProcessLockRequest { Expiration = 0 };
+ var content = JsonContent.Create(request);
+
+ HttpResponseMessage response = await _client.PatchAsync(token, apiUrl, content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw await PlatformHttpException.CreateAsync(response);
+ }
+ }
+}
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
new file mode 100644
index 0000000000..c8e816eeba
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
@@ -0,0 +1,6 @@
+namespace Altinn.App.Core.Internal.Process.ProcessLock;
+
+internal sealed class ProcessLockRequest
+{
+ public int Expiration { get; set; }
+}
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs
new file mode 100644
index 0000000000..7f7f2d40cc
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs
@@ -0,0 +1,6 @@
+namespace Altinn.App.Core.Internal.Process.ProcessLock;
+
+internal sealed class ProcessLockResponse
+{
+ public Guid LockId { get; set; }
+}
diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj
index 9b0ae996d7..098b999643 100644
--- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj
+++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj
@@ -18,6 +18,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs b/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs
index 04d4bf5347..3eab426934 100644
--- a/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs
+++ b/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs
@@ -86,10 +86,13 @@ public class CustomConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) => typeToConvert is not null;
- public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ public override System.Text.Json.Serialization.JsonConverter? CreateConverter(
+ Type typeToConvert,
+ JsonSerializerOptions options
+ )
{
var converterType = typeof(CustomConverter<>).MakeGenericType(typeToConvert);
- return (JsonConverter)Activator.CreateInstance(converterType)!;
+ return (System.Text.Json.Serialization.JsonConverter)Activator.CreateInstance(converterType)!;
}
}
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
new file mode 100644
index 0000000000..f8c3bd9217
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
@@ -0,0 +1,388 @@
+using System.Net;
+using System.Net.Http.Json;
+using Altinn.App.Api.Infrastructure.Middleware;
+using Altinn.App.Core.Configuration;
+using Altinn.App.Core.Infrastructure.Clients.Storage;
+using Altinn.App.Core.Internal.Process.ProcessLock;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using WireMock.Matchers.Request;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+
+namespace Altinn.App.Api.Tests.Middleware;
+
+public sealed class ProcessLockMiddlewareTests
+{
+ private sealed record Fixture(IHost Host, WireMockServer Server) : IDisposable
+ {
+ public readonly Guid InstanceGuid = Guid.NewGuid();
+ public readonly int InstanceOwnerPartyId = 12345;
+ private const string RuntimeCookieName = "test-cookie";
+ private const string BearerToken = "test-token";
+
+ public static Fixture Create(Action? registerCustomAppServices = null)
+ {
+ var server = WireMockServer.Start();
+
+ var host = new HostBuilder()
+ .ConfigureWebHost(webBuilder =>
+ {
+ webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddRouting();
+
+ services.Configure(settings =>
+ {
+ var testUrl = server.Url ?? throw new Exception("Missing server URL");
+ settings.ApiStorageEndpoint =
+ testUrl + new Uri(settings.ApiStorageEndpoint).PathAndQuery;
+ });
+
+ services.Configure(settings => settings.RuntimeCookieName = RuntimeCookieName);
+
+ services.AddHttpClient();
+
+ services.AddHttpContextAccessor();
+
+ registerCustomAppServices?.Invoke(services);
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseMiddleware();
+ app.UseEndpoints(endpoints =>
+ {
+ // Endpoint with lock
+ endpoints
+ .MapGet(
+ "/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/test",
+ () => "Success"
+ )
+ .WithMetadata(new EnableProcessLockAttribute());
+
+ // Endpoint without lock
+ endpoints.MapGet("/without-lock", () => "No lock required");
+
+ // Endpoint with lock that throws exception
+ endpoints
+ .MapGet(
+ "/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/test-exception",
+ _ => throw new InvalidOperationException("Test exception")
+ )
+ .WithMetadata(new EnableProcessLockAttribute());
+
+ // Endpoint with lock but no route parameters
+ endpoints
+ .MapGet("/invalid-route", () => Results.Ok())
+ .WithMetadata(new EnableProcessLockAttribute());
+ });
+ });
+ })
+ .Start();
+
+ return new Fixture(host, server);
+ }
+
+ public void Dispose()
+ {
+ Server.Stop();
+ Server.Dispose();
+ Host.Dispose();
+ }
+
+ public HttpClient GetTestClient()
+ {
+ var httpClient = Host.GetTestClient();
+ httpClient.DefaultRequestHeaders.Add("cookie", $"{RuntimeCookieName}={BearerToken}");
+
+ return httpClient;
+ }
+
+ public IRequestBuilder GetAcquireLockRequestBuilder()
+ {
+ return Request
+ .Create()
+ .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{InstanceGuid}/process/lock")
+ .UsingPost()
+ .WithHeader("Authorization", $"Bearer {BearerToken}");
+ }
+
+ public IRequestBuilder GetReleaseLockRequestBuilder(Guid lockId)
+ {
+ return Request
+ .Create()
+ .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{InstanceGuid}/process/lock/{lockId}")
+ .UsingPatch()
+ .WithHeader("Authorization", $"Bearer {BearerToken}");
+ }
+ }
+
+ [Fact]
+ public async Task HappyPath()
+ {
+ using var fixture = Fixture.Create();
+
+ var lockId = Guid.NewGuid();
+
+ var acquireLockRequestBuilder = fixture.GetAcquireLockRequestBuilder();
+ var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockId);
+
+ fixture
+ .Server.Given(acquireLockRequestBuilder)
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ );
+
+ fixture
+ .Server.Given(releaseLockRequestBuilder)
+ .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
+
+ var response = await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal("Success", content);
+
+ var requests = fixture.Server.LogEntries;
+ Assert.Equal(2, requests.Count);
+
+ var acquireMatchResult = new RequestMatchResult();
+ acquireLockRequestBuilder.GetMatchingScore(requests[0].RequestMessage, acquireMatchResult);
+ Assert.True(acquireMatchResult.IsPerfectMatch);
+
+ var releaseMatchResult = new RequestMatchResult();
+ releaseLockRequestBuilder.GetMatchingScore(requests[1].RequestMessage, releaseMatchResult);
+ Assert.True(releaseMatchResult.IsPerfectMatch);
+ }
+
+ [Fact]
+ public async Task EndpointWithoutAttribute_SkipsMiddleware()
+ {
+ using var fixture = Fixture.Create();
+
+ var response = await fixture.GetTestClient().GetAsync("/without-lock");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal("No lock required", content);
+
+ Assert.Empty(fixture.Server.LogEntries);
+ }
+
+ [Fact]
+ public async Task LockReleasedOnException()
+ {
+ using var fixture = Fixture.Create();
+
+ var lockId = Guid.NewGuid();
+
+ fixture
+ .Server.Given(fixture.GetAcquireLockRequestBuilder())
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ );
+
+ fixture
+ .Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
+ .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
+
+ await Assert.ThrowsAsync(async () =>
+ await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test-exception")
+ );
+
+ var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
+ Assert.Single(releaseRequests);
+ }
+
+ [Fact]
+ public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
+ {
+ var lockId = Guid.NewGuid();
+ const int customExpirationSeconds = 120;
+
+ using var fixture = Fixture.Create(services =>
+ services.Configure(options =>
+ options.Expiration = TimeSpan.FromSeconds(customExpirationSeconds)
+ )
+ );
+
+ fixture
+ .Server.Given(fixture.GetAcquireLockRequestBuilder())
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ );
+
+ fixture
+ .Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
+ .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
+
+ var response = await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var acquireRequests = fixture.Server.FindLogEntries(fixture.GetAcquireLockRequestBuilder());
+ Assert.Single(acquireRequests);
+ var requestBody = acquireRequests[0].RequestMessage.Body;
+ Assert.Contains($"\"expiration\":{customExpirationSeconds}", requestBody);
+ }
+
+ [Fact]
+ public async Task MissingRouteParameters_ThrowsException()
+ {
+ using var fixture = Fixture.Create();
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ await fixture.GetTestClient().GetAsync("/invalid-route")
+ );
+
+ Assert.Contains("Unable to extract instance identifiers.", exception.Message);
+
+ Assert.Empty(fixture.Server.LogEntries);
+ }
+
+ [Fact]
+ public async Task LockReleaseFailure_DoesNotAffectResponse()
+ {
+ using var fixture = Fixture.Create();
+
+ var lockId = Guid.NewGuid();
+
+ fixture
+ .Server.Given(fixture.GetAcquireLockRequestBuilder())
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ );
+
+ fixture
+ .Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
+ .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.InternalServerError));
+
+ var response = await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal("Success", content);
+
+ var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
+ Assert.Single(releaseRequests);
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.Conflict)]
+ [InlineData(HttpStatusCode.NotFound)]
+ [InlineData(HttpStatusCode.InternalServerError)]
+ public async Task StorageApiError_ReturnsCorrectStatusCode(HttpStatusCode storageStatusCode)
+ {
+ using var fixture = Fixture.Create();
+
+ fixture
+ .Server.Given(fixture.GetAcquireLockRequestBuilder())
+ .RespondWith(Response.Create().WithStatusCode(storageStatusCode));
+
+ var response = await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+
+ Assert.Equal(storageStatusCode, response.StatusCode);
+
+ var problemDetails = await response.Content.ReadFromJsonAsync();
+
+ Assert.NotNull(problemDetails);
+ Assert.Equal("Failed to acquire lock.", problemDetails.Title);
+ Assert.Equal((int)storageStatusCode, problemDetails.Status);
+
+ Assert.Single(fixture.Server.LogEntries);
+ }
+
+ [Fact]
+ public async Task NullResponseBody_ReturnsProblemDetails()
+ {
+ using var fixture = Fixture.Create();
+
+ fixture
+ .Server.Given(fixture.GetAcquireLockRequestBuilder())
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody("null")
+ );
+
+ var response = await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+
+ var problemDetails = await response.Content.ReadFromJsonAsync();
+
+ Assert.NotNull(problemDetails);
+ Assert.Equal("Failed to acquire lock.", problemDetails.Title);
+ Assert.Equal("The response from the lock acquisition endpoint was not expected.", problemDetails.Detail);
+ Assert.Equal((int)HttpStatusCode.InternalServerError, problemDetails.Status);
+
+ Assert.Single(fixture.Server.LogEntries);
+ }
+
+ [Fact]
+ public async Task EmptyJsonResponseBody_ReturnsProblemDetails()
+ {
+ using var fixture = Fixture.Create();
+
+ fixture
+ .Server.Given(fixture.GetAcquireLockRequestBuilder())
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody("{}")
+ );
+
+ var response = await fixture
+ .GetTestClient()
+ .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+
+ var problemDetails = await response.Content.ReadFromJsonAsync();
+
+ Assert.NotNull(problemDetails);
+ Assert.Equal("Failed to acquire lock.", problemDetails.Title);
+ Assert.Equal("The response from the lock acquisition endpoint was not expected.", problemDetails.Detail);
+ Assert.Equal((int)HttpStatusCode.InternalServerError, problemDetails.Status);
+
+ Assert.Single(fixture.Server.LogEntries);
+ }
+}
From 8d50972b57550dfd7deb066adefc788e30b888a0 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Tue, 25 Nov 2025 13:53:15 +0100
Subject: [PATCH 02/17] Use snapshot testing
---
...guration_UsedInStorageApiCall.verified.txt | 3 ++
...nseBody_ReturnsProblemDetails.verified.txt | 16 ++++++++
...uteParameters_ThrowsException.verified.txt | 14 +++++++
...nseBody_ReturnsProblemDetails.verified.txt | 16 ++++++++
...de_storageStatusCode=Conflict.verified.txt | 16 ++++++++
...tatusCode=InternalServerError.verified.txt | 16 ++++++++
...de_storageStatusCode=NotFound.verified.txt | 16 ++++++++
.../Middleware/ProcessLockMiddlewareTests.cs | 40 +++++--------------
8 files changed, 106 insertions(+), 31 deletions(-)
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
create mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
new file mode 100644
index 0000000000..6c474e41df
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
@@ -0,0 +1,3 @@
+{
+ RequestBody: {"expiration":120}
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt
new file mode 100644
index 0000000000..a44cbff789
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt
@@ -0,0 +1,16 @@
+{
+ Response: {
+ Status: 500 Internal Server Error,
+ Content: {
+ Headers: {
+ Content-Type: application/problem+json
+ },
+ Value: {
+ type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
+ title: Failed to acquire lock.,
+ status: 500,
+ detail: The response from the lock acquisition endpoint was not expected.
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt
new file mode 100644
index 0000000000..ccd8a701a9
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt
@@ -0,0 +1,14 @@
+{
+ Exception: {
+ $type: InvalidOperationException,
+ Type: InvalidOperationException,
+ Message: Unable to extract instance identifiers.,
+ StackTrace:
+at Altinn.App.Api.Infrastructure.Middleware.ProcessLockMiddleware.Invoke(HttpContext context)
+--- End of stack trace from previous location ---
+at Microsoft.AspNetCore.TestHost.ClientHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt
new file mode 100644
index 0000000000..a44cbff789
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt
@@ -0,0 +1,16 @@
+{
+ Response: {
+ Status: 500 Internal Server Error,
+ Content: {
+ Headers: {
+ Content-Type: application/problem+json
+ },
+ Value: {
+ type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
+ title: Failed to acquire lock.,
+ status: 500,
+ detail: The response from the lock acquisition endpoint was not expected.
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
new file mode 100644
index 0000000000..cbdb7d4bce
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
@@ -0,0 +1,16 @@
+{
+ Response: {
+ Status: 409 Conflict,
+ Content: {
+ Headers: {
+ Content-Type: application/problem+json
+ },
+ Value: {
+ type: https://tools.ietf.org/html/rfc9110#section-15.5.10,
+ title: Failed to acquire lock.,
+ status: 409,
+ detail: 409 - Conflict -
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
new file mode 100644
index 0000000000..a7f5494031
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
@@ -0,0 +1,16 @@
+{
+ Response: {
+ Status: 500 Internal Server Error,
+ Content: {
+ Headers: {
+ Content-Type: application/problem+json
+ },
+ Value: {
+ type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
+ title: Failed to acquire lock.,
+ status: 500,
+ detail: 500 - Internal Server Error -
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
new file mode 100644
index 0000000000..3845a2f396
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
@@ -0,0 +1,16 @@
+{
+ Response: {
+ Status: 404 Not Found,
+ Content: {
+ Headers: {
+ Content-Type: application/problem+json
+ },
+ Value: {
+ type: https://tools.ietf.org/html/rfc9110#section-15.5.5,
+ title: Failed to acquire lock.,
+ status: 404,
+ detail: 404 - Not Found -
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
index f8c3bd9217..a17988bc61 100644
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
@@ -1,5 +1,4 @@
using System.Net;
-using System.Net.Http.Json;
using Altinn.App.Api.Infrastructure.Middleware;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Infrastructure.Clients.Storage;
@@ -7,7 +6,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -248,7 +246,8 @@ public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
var acquireRequests = fixture.Server.FindLogEntries(fixture.GetAcquireLockRequestBuilder());
Assert.Single(acquireRequests);
var requestBody = acquireRequests[0].RequestMessage.Body;
- Assert.Contains($"\"expiration\":{customExpirationSeconds}", requestBody);
+
+ await Verify(new { RequestBody = requestBody });
}
[Fact]
@@ -260,9 +259,8 @@ public async Task MissingRouteParameters_ThrowsException()
await fixture.GetTestClient().GetAsync("/invalid-route")
);
- Assert.Contains("Unable to extract instance identifiers.", exception.Message);
-
Assert.Empty(fixture.Server.LogEntries);
+ await Verify(new { Exception = exception });
}
[Fact]
@@ -313,15 +311,9 @@ public async Task StorageApiError_ReturnsCorrectStatusCode(HttpStatusCode storag
.GetTestClient()
.GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
- Assert.Equal(storageStatusCode, response.StatusCode);
-
- var problemDetails = await response.Content.ReadFromJsonAsync();
-
- Assert.NotNull(problemDetails);
- Assert.Equal("Failed to acquire lock.", problemDetails.Title);
- Assert.Equal((int)storageStatusCode, problemDetails.Status);
-
Assert.Single(fixture.Server.LogEntries);
+
+ await Verify(new { Response = response }).UseParameters(storageStatusCode);
}
[Fact]
@@ -343,16 +335,9 @@ public async Task NullResponseBody_ReturnsProblemDetails()
.GetTestClient()
.GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
- Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
-
- var problemDetails = await response.Content.ReadFromJsonAsync();
-
- Assert.NotNull(problemDetails);
- Assert.Equal("Failed to acquire lock.", problemDetails.Title);
- Assert.Equal("The response from the lock acquisition endpoint was not expected.", problemDetails.Detail);
- Assert.Equal((int)HttpStatusCode.InternalServerError, problemDetails.Status);
-
Assert.Single(fixture.Server.LogEntries);
+
+ await Verify(new { Response = response });
}
[Fact]
@@ -374,15 +359,8 @@ public async Task EmptyJsonResponseBody_ReturnsProblemDetails()
.GetTestClient()
.GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
- Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
-
- var problemDetails = await response.Content.ReadFromJsonAsync();
-
- Assert.NotNull(problemDetails);
- Assert.Equal("Failed to acquire lock.", problemDetails.Title);
- Assert.Equal("The response from the lock acquisition endpoint was not expected.", problemDetails.Detail);
- Assert.Equal((int)HttpStatusCode.InternalServerError, problemDetails.Status);
-
Assert.Single(fixture.Server.LogEntries);
+
+ await Verify(new { Response = response });
}
}
From afa3dd715bc9fadd66ca105223ff17584522aeb3 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Tue, 25 Nov 2025 15:14:49 +0100
Subject: [PATCH 03/17] Remove unused method
---
.../Features/Telemetry/Telemetry.ProcessLockClient.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
index eda8e56812..6131a40c29 100644
--- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
+++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
@@ -4,8 +4,6 @@ namespace Altinn.App.Core.Features;
partial class Telemetry
{
- internal Activity? StartAqzActivity() => ActivitySource.StartActivity("ProcessClient.GetProcessDefinition");
-
internal Activity? StartAcquireProcessLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
{
var activity = ActivitySource.StartActivity("AcquireProcessLock");
From d84fe59f84dcfcffe9125bd02ac5b0ef05bbe8a4 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Tue, 25 Nov 2025 15:53:56 +0100
Subject: [PATCH 04/17] Log exception
---
.../Clients/Storage/ProcessLockClient.cs | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
index c3a21b2ed2..63958b6675 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
@@ -8,6 +8,7 @@
using Altinn.App.Core.Internal.Process.ProcessLock;
using AltinnCore.Authentication.Utils;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Altinn.App.Core.Infrastructure.Clients.Storage;
@@ -15,6 +16,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage;
internal sealed class ProcessLockClient
{
private readonly AppSettings _appSettings;
+ private readonly ILogger _logger;
private readonly HttpClient _client;
private readonly Telemetry? _telemetry;
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -22,12 +24,14 @@ internal sealed class ProcessLockClient
public ProcessLockClient(
IOptions platformSettings,
IOptions appSettings,
+ ILogger logger,
IHttpContextAccessor httpContextAccessor,
HttpClient httpClient,
Telemetry? telemetry = null
)
{
_appSettings = appSettings.Value;
+ _logger = logger;
_httpContextAccessor = httpContextAccessor;
httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint);
httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey);
@@ -48,7 +52,7 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
var request = new ProcessLockRequest { Expiration = (int)expiration.TotalSeconds };
var content = JsonContent.Create(request);
- HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content);
+ var response = await _client.PostAsync(token, apiUrl, content);
if (!response.IsSuccessStatusCode)
{
@@ -61,9 +65,9 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
var lockResponse = await response.Content.ReadFromJsonAsync();
lockId = lockResponse?.LockId;
}
- catch
+ catch (Exception e)
{
- // Throw exception below
+ _logger.LogError(e, "Error reading response from the lock acquisition endpoint.");
}
if (lockId is null || lockId.Value == Guid.Empty)
@@ -89,7 +93,7 @@ public async Task ReleaseProcessLock(Guid instanceGuid, int instanceOwnerPartyId
var request = new ProcessLockRequest { Expiration = 0 };
var content = JsonContent.Create(request);
- HttpResponseMessage response = await _client.PatchAsync(token, apiUrl, content);
+ var response = await _client.PatchAsync(token, apiUrl, content);
if (!response.IsSuccessStatusCode)
{
From c7d71b6e3060244d139043dcdab29349e9c4308b Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Tue, 25 Nov 2025 16:49:34 +0100
Subject: [PATCH 05/17] Update public api test
---
...iTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
index f62c1bff3a..466319ba09 100644
--- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
+++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
@@ -241,6 +241,7 @@ namespace Altinn.App.Core.Extensions
public static System.Threading.Tasks.Task DeleteAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, string? platformAccessToken = null, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task GetAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, string? platformAccessToken = null, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task GetStreamingAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, string? platformAccessToken = null, System.Threading.CancellationToken cancellationToken = default) { }
+ public static System.Threading.Tasks.Task PatchAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, System.Net.Http.HttpContent? content, string? platformAccessToken = null, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task PostAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, System.Net.Http.HttpContent? content, string? platformAccessToken = null, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task PutAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, System.Net.Http.HttpContent? content, string? platformAccessToken = null, System.Threading.CancellationToken cancellationToken = default) { }
}
From 6d3a10e158cd3137ee13d0a64b5dd425a11d42e6 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Wed, 26 Nov 2025 19:38:58 +0100
Subject: [PATCH 06/17] Use PlatformHttpResponseSnapshotException
---
.../PlatformHttpResponseSnapshotException.cs | 116 +++++++++++-------
.../Clients/Storage/ProcessLockClient.cs | 14 +--
...de_storageStatusCode=Conflict.verified.txt | 2 +-
...tatusCode=InternalServerError.verified.txt | 2 +-
...de_storageStatusCode=NotFound.verified.txt | 2 +-
5 files changed, 79 insertions(+), 57 deletions(-)
diff --git a/src/Altinn.App.Core/Helpers/PlatformHttpResponseSnapshotException.cs b/src/Altinn.App.Core/Helpers/PlatformHttpResponseSnapshotException.cs
index 21eadcf85b..3f08ebacd3 100644
--- a/src/Altinn.App.Core/Helpers/PlatformHttpResponseSnapshotException.cs
+++ b/src/Altinn.App.Core/Helpers/PlatformHttpResponseSnapshotException.cs
@@ -78,69 +78,91 @@ public static async Task CreateAndDispose
cancellationToken
);
- string headers = FlattenHeaders(response.Headers, response.Content?.Headers, response.TrailingHeaders);
string message = BuildMessage((int)response.StatusCode, response.ReasonPhrase, content, truncated);
- // Build a sanitized, non-streaming HttpResponseMessage for the base class
- var safeResponse = new HttpResponseMessage(response.StatusCode)
+ return Create(message, response, content, truncated);
+ }
+ finally
+ {
+ try
{
- ReasonPhrase = response.ReasonPhrase,
- Version = response.Version,
- };
-
- // Copy normal headers
- foreach (KeyValuePair> h in response.Headers)
+ response.Dispose();
+ }
+ catch
{
- if (_redactedHeaders.Contains(h.Key))
- {
- safeResponse.Headers.TryAddWithoutValidation(h.Key, [Redacted]);
- }
- else
- {
- safeResponse.Headers.TryAddWithoutValidation(h.Key, h.Value);
- }
+ /* ignore dispose failures */
}
+ }
+ }
+
+ ///
+ /// Creates a new by snapshotting
+ /// the provided into immutable string values,
+ /// constructing a sanitized clone for the base class.
+ ///
+ /// The exception message.
+ /// The HTTP response to snapshot.
+ /// The response body content as a string (possibly truncated).
+ /// Whether the content was truncated.
+ /// The constructed .
+ public static PlatformHttpResponseSnapshotException Create(
+ string message,
+ HttpResponseMessage response,
+ string? content = null,
+ bool contentTruncated = false
+ )
+ {
+ string headers = FlattenHeaders(response.Headers, response.Content?.Headers, response.TrailingHeaders);
+ content ??= string.Empty;
- // Attach a diagnostic snapshot body for legacy consumers
- StringContent safeContent = new StringContent(content, Encoding.UTF8);
- safeContent.Headers.ContentType = response.Content?.Headers?.ContentType;
- safeResponse.Content = safeContent;
+ // Build a sanitized, non-streaming HttpResponseMessage for the base class
+ var safeResponse = new HttpResponseMessage(response.StatusCode)
+ {
+ ReasonPhrase = response.ReasonPhrase,
+ Version = response.Version,
+ };
- // Copy trailing headers if present (HTTP/2+)
- foreach (KeyValuePair> h in response.TrailingHeaders)
+ // Copy normal headers
+ foreach (KeyValuePair> h in response.Headers)
+ {
+ if (_redactedHeaders.Contains(h.Key))
{
- if (_redactedHeaders.Contains(h.Key))
- {
- safeResponse.TrailingHeaders.TryAddWithoutValidation(h.Key, [Redacted]);
- }
- else
- {
- safeResponse.TrailingHeaders.TryAddWithoutValidation(h.Key, h.Value);
- }
+ safeResponse.Headers.TryAddWithoutValidation(h.Key, [Redacted]);
+ }
+ else
+ {
+ safeResponse.Headers.TryAddWithoutValidation(h.Key, h.Value);
}
-
- return new PlatformHttpResponseSnapshotException(
- safeResponse,
- statusCode: (int)response.StatusCode,
- reasonPhrase: response.ReasonPhrase,
- httpVersion: response.Version?.ToString() ?? string.Empty,
- headers: headers,
- content: content,
- contentTruncated: truncated,
- message: message
- );
}
- finally
+
+ // Attach a diagnostic snapshot body for legacy consumers
+ StringContent safeContent = new StringContent(content, Encoding.UTF8);
+ safeContent.Headers.ContentType = response.Content?.Headers?.ContentType;
+ safeResponse.Content = safeContent;
+
+ // Copy trailing headers if present (HTTP/2+)
+ foreach (KeyValuePair> h in response.TrailingHeaders)
{
- try
+ if (_redactedHeaders.Contains(h.Key))
{
- response.Dispose();
+ safeResponse.TrailingHeaders.TryAddWithoutValidation(h.Key, [Redacted]);
}
- catch
+ else
{
- /* ignore dispose failures */
+ safeResponse.TrailingHeaders.TryAddWithoutValidation(h.Key, h.Value);
}
}
+
+ return new PlatformHttpResponseSnapshotException(
+ safeResponse,
+ statusCode: (int)response.StatusCode,
+ reasonPhrase: response.ReasonPhrase,
+ httpVersion: response.Version?.ToString() ?? string.Empty,
+ headers: headers,
+ content: content,
+ contentTruncated: contentTruncated,
+ message: message
+ );
}
///
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
index 63958b6675..2d95c69b9a 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
@@ -52,11 +52,11 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
var request = new ProcessLockRequest { Expiration = (int)expiration.TotalSeconds };
var content = JsonContent.Create(request);
- var response = await _client.PostAsync(token, apiUrl, content);
+ using var response = await _client.PostAsync(token, apiUrl, content);
if (!response.IsSuccessStatusCode)
{
- throw await PlatformHttpException.CreateAsync(response);
+ throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response);
}
Guid? lockId = null;
@@ -72,9 +72,9 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
if (lockId is null || lockId.Value == Guid.Empty)
{
- throw new PlatformHttpException(
- response,
- "The response from the lock acquisition endpoint was not expected."
+ throw PlatformHttpResponseSnapshotException.Create(
+ "The response from the lock acquisition endpoint was not expected.",
+ response
);
}
@@ -93,11 +93,11 @@ public async Task ReleaseProcessLock(Guid instanceGuid, int instanceOwnerPartyId
var request = new ProcessLockRequest { Expiration = 0 };
var content = JsonContent.Create(request);
- var response = await _client.PatchAsync(token, apiUrl, content);
+ using var response = await _client.PatchAsync(token, apiUrl, content);
if (!response.IsSuccessStatusCode)
{
- throw await PlatformHttpException.CreateAsync(response);
+ throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response);
}
}
}
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
index cbdb7d4bce..a6b96dea8f 100644
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
@@ -9,7 +9,7 @@
type: https://tools.ietf.org/html/rfc9110#section-15.5.10,
title: Failed to acquire lock.,
status: 409,
- detail: 409 - Conflict -
+ detail: 409 Conflict
}
}
}
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
index a7f5494031..6dfddf8227 100644
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
@@ -9,7 +9,7 @@
type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
title: Failed to acquire lock.,
status: 500,
- detail: 500 - Internal Server Error -
+ detail: 500 Internal Server Error
}
}
}
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
index 3845a2f396..417802ed99 100644
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
@@ -9,7 +9,7 @@
type: https://tools.ietf.org/html/rfc9110#section-15.5.5,
title: Failed to acquire lock.,
status: 404,
- detail: 404 - Not Found -
+ detail: 404 Not Found
}
}
}
From 480012660def29a45875009edf3c0a5064dd316a Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Sun, 30 Nov 2025 18:58:48 +0100
Subject: [PATCH 07/17] Change Expiration to TtlSeconds
---
.../Infrastructure/Clients/Storage/ProcessLockClient.cs | 4 ++--
.../Internal/Process/ProcessLock/ProcessLockRequest.cs | 2 +-
...mExpirationConfiguration_UsedInStorageApiCall.verified.txt | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
index 2d95c69b9a..dc46850ce2 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
@@ -49,7 +49,7 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
_appSettings.RuntimeCookieName
);
- var request = new ProcessLockRequest { Expiration = (int)expiration.TotalSeconds };
+ var request = new ProcessLockRequest { TtlSeconds = (int)expiration.TotalSeconds };
var content = JsonContent.Create(request);
using var response = await _client.PostAsync(token, apiUrl, content);
@@ -90,7 +90,7 @@ public async Task ReleaseProcessLock(Guid instanceGuid, int instanceOwnerPartyId
_appSettings.RuntimeCookieName
);
- var request = new ProcessLockRequest { Expiration = 0 };
+ var request = new ProcessLockRequest { TtlSeconds = 0 };
var content = JsonContent.Create(request);
using var response = await _client.PatchAsync(token, apiUrl, content);
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
index c8e816eeba..3d55e85753 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
@@ -2,5 +2,5 @@ namespace Altinn.App.Core.Internal.Process.ProcessLock;
internal sealed class ProcessLockRequest
{
- public int Expiration { get; set; }
+ public int TtlSeconds { get; set; }
}
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
index 6c474e41df..355d4cdb15 100644
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
+++ b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
@@ -1,3 +1,3 @@
{
- RequestBody: {"expiration":120}
+ RequestBody: {"ttlSeconds":120}
}
\ No newline at end of file
From 3c685f4c208c28bfad945e2567a097ed1f198e01 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Sun, 30 Nov 2025 19:41:10 +0100
Subject: [PATCH 08/17] Enable lock for /process/next and add integration test
---
.../Controllers/ProcessController.cs | 2 +
.../WebApplicationBuilderExtensions.cs | 1 +
.../Extensions/ServiceCollectionExtensions.cs | 1 +
.../ProcessLock/ProcessLockTests.cs | 50 ++++++++++++++
.../services/WaitForReleaseProcessTaskEnd.cs | 65 +++++++++++++++++++
5 files changed, 119 insertions(+)
create mode 100644 test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs
create mode 100644 test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs
diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs
index af08623f2b..1ba428de9d 100644
--- a/src/Altinn.App.Api/Controllers/ProcessController.cs
+++ b/src/Altinn.App.Api/Controllers/ProcessController.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Net;
using Altinn.App.Api.Infrastructure.Filters;
+using Altinn.App.Api.Infrastructure.Middleware;
using Altinn.App.Api.Models;
using Altinn.App.Core.Constants;
using Altinn.App.Core.Helpers;
@@ -253,6 +254,7 @@ [FromRoute] Guid instanceGuid
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
+ [EnableProcessLock]
public async Task> NextElement(
[FromRoute] string org,
[FromRoute] string app,
diff --git a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs
index c3e5ed5819..be3d2fac61 100644
--- a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs
+++ b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs
@@ -28,6 +28,7 @@ public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicat
app.UseAuthorization();
app.UseTelemetryEnricher();
app.UseScopeAuthorization();
+ app.UseMiddleware();
app.UseEndpoints(endpoints =>
{
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index e463ded3a9..20ad641c19 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -114,6 +114,7 @@ IWebHostEnvironment env
services.AddHttpClient();
#pragma warning restore CS0618 // Type or member is obsolete
services.AddHttpClient();
+ services.AddHttpClient();
services.AddHttpClient();
services.AddHttpClient();
diff --git a/test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs b/test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs
new file mode 100644
index 0000000000..c20f410f82
--- /dev/null
+++ b/test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs
@@ -0,0 +1,50 @@
+using Altinn.App.Api.Models;
+using Altinn.Platform.Storage.Interface.Models;
+using Xunit.Abstractions;
+
+namespace Altinn.App.Integration.Tests.ProcessLock;
+
+[Trait("Category", "Integration")]
+public sealed class ProcessLockTests(ITestOutputHelper _output, AppFixtureClassFixture _classFixture)
+ : IClassFixture
+{
+ [Fact]
+ public async Task ProcessNext_ConcurrentRequests_OneRequestGetsConflict()
+ {
+ await using var fixtureScope = await _classFixture.Get(_output, TestApps.Basic, "process-lock");
+ var fixture = fixtureScope.Fixture;
+
+ var client = fixture.GetAppClient();
+
+ var resetResponse = await client.PostAsync("/test/process-lock/reset", null);
+ resetResponse.EnsureSuccessStatusCode();
+
+ var token = await fixture.Auth.GetUserToken(userId: 1337);
+
+ using var instantiationResponse = await fixture.Instances.PostSimplified(
+ token,
+ new InstansiationInstance { InstanceOwner = new InstanceOwner { PartyId = "501337" } }
+ );
+ using var readInstantiationResponse = await instantiationResponse.Read();
+ var instance = readInstantiationResponse.Data.Model;
+ Assert.NotNull(instance);
+
+ Task[] processNextTasks =
+ [
+ fixture.Instances.ProcessNext(token, readInstantiationResponse),
+ fixture.Instances.ProcessNext(token, readInstantiationResponse),
+ ];
+
+ var conflictResponseTask = await Task.WhenAny(processNextTasks);
+ using var conflictResponse = await conflictResponseTask;
+
+ Assert.Equal(System.Net.HttpStatusCode.Conflict, conflictResponse.Response.StatusCode);
+
+ var releaseResponse = await client.PostAsync("/test/process-lock/release-wait", null);
+ releaseResponse.EnsureSuccessStatusCode();
+
+ using var successRequestResponse = await processNextTasks.First(x => x != conflictResponseTask);
+
+ successRequestResponse.Response.EnsureSuccessStatusCode();
+ }
+}
diff --git a/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs b/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs
new file mode 100644
index 0000000000..1d6605269f
--- /dev/null
+++ b/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.App.Core.Features;
+using Altinn.Platform.Storage.Interface.Models;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using TestApp.Shared;
+
+namespace Altinn.App.Integration.Tests.Scenarios.ProcessLock;
+
+public sealed class WaitForReleaseProcessTaskEnd : IProcessTaskEnd
+{
+ private static TaskCompletionSource _signal = new();
+
+ public Task End(string taskId, Instance instance)
+ {
+ return _signal.Task;
+ }
+
+ public static void Release()
+ {
+ _signal.TrySetResult();
+ }
+
+ public static void Reset()
+ {
+ _signal.TrySetResult();
+ _signal = new TaskCompletionSource();
+ }
+}
+
+public sealed class WaitForReleaseProcessTaskEndEndpoints : IEndpointConfigurator
+{
+ public void ConfigureEndpoints(WebApplication app)
+ {
+ app.MapPost(
+ "/test/process-lock/release-wait",
+ () =>
+ {
+ WaitForReleaseProcessTaskEnd.Release();
+ return Results.Ok();
+ }
+ );
+
+ app.MapPost(
+ "/test/process-lock/reset",
+ () =>
+ {
+ WaitForReleaseProcessTaskEnd.Reset();
+ return Results.Ok();
+ }
+ );
+ }
+}
+
+public static class ServiceRegistration
+{
+ public static void RegisterServices(IServiceCollection services)
+ {
+ services.AddTransient();
+ services.AddSingleton();
+ }
+}
From 9debb7854d5795ffdaeb0ab8bfd3a262f9fe8b8a Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Mon, 8 Dec 2025 15:40:41 +0100
Subject: [PATCH 09/17] Replace middleware by async disposable
---
.../Controllers/ProcessController.cs | 6 +-
.../WebApplicationBuilderExtensions.cs | 1 -
.../Middleware/EnableProcessLockAttribute.cs | 4 -
.../Middleware/ProcessLockMiddleware.cs | 120 --------
.../Extensions/ServiceCollectionExtensions.cs | 3 +
.../ProcessLock}/ProcessLockOptions.cs | 2 +-
.../Process/ProcessLock/ProcessLocker.cs | 69 +++++
.../Altinn.App.Api.Tests.csproj | 1 -
...nseBody_ReturnsProblemDetails.verified.txt | 16 --
...uteParameters_ThrowsException.verified.txt | 14 -
...nseBody_ReturnsProblemDetails.verified.txt | 16 --
...de_storageStatusCode=Conflict.verified.txt | 16 --
...tatusCode=InternalServerError.verified.txt | 16 --
...de_storageStatusCode=NotFound.verified.txt | 16 --
...uration_UsedInStorageApiCall.verified.txt} | 0
...y_ThrowsPlatformHttpException.verified.txt | 31 +++
...nceId_ThrowsArgumentException.verified.txt | 11 +
...y_ThrowsPlatformHttpException.verified.txt | 31 +++
...on_storageStatusCode=Conflict.verified.txt | 28 ++
...tatusCode=InternalServerError.verified.txt | 28 ++
...on_storageStatusCode=NotFound.verified.txt | 28 ++
.../Internal/Process/ProcessLockTests.cs} | 258 ++++++++----------
22 files changed, 353 insertions(+), 362 deletions(-)
delete mode 100644 src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs
delete mode 100644 src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs
rename src/{Altinn.App.Api/Infrastructure/Middleware => Altinn.App.Core/Internal/Process/ProcessLock}/ProcessLockOptions.cs (67%)
create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
delete mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt
delete mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt
delete mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt
delete mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
delete mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
delete mode 100644 test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
rename test/{Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt => Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt} (100%)
create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
rename test/{Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs => Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs} (51%)
diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs
index 1ba428de9d..7684d58fcb 100644
--- a/src/Altinn.App.Api/Controllers/ProcessController.cs
+++ b/src/Altinn.App.Api/Controllers/ProcessController.cs
@@ -1,7 +1,6 @@
using System.Globalization;
using System.Net;
using Altinn.App.Api.Infrastructure.Filters;
-using Altinn.App.Api.Infrastructure.Middleware;
using Altinn.App.Api.Models;
using Altinn.App.Core.Constants;
using Altinn.App.Core.Helpers;
@@ -10,6 +9,7 @@
using Altinn.App.Core.Internal.Process;
using Altinn.App.Core.Internal.Process.Elements;
using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties;
+using Altinn.App.Core.Internal.Process.ProcessLock;
using Altinn.App.Core.Internal.Validation;
using Altinn.App.Core.Models.Process;
using Altinn.App.Core.Models.Validation;
@@ -41,6 +41,7 @@ public class ProcessController : ControllerBase
private readonly IProcessEngineAuthorizer _processEngineAuthorizer;
private readonly IValidationService _validationService;
private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer;
+ private readonly ProcessLocker _processLocker;
///
/// Initializes a new instance of the
@@ -66,6 +67,7 @@ IProcessEngineAuthorizer processEngineAuthorizer
_processEngineAuthorizer = processEngineAuthorizer;
_validationService = validationService;
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService();
+ _processLocker = serviceProvider.GetRequiredService();
}
///
@@ -254,7 +256,6 @@ [FromRoute] Guid instanceGuid
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
- [EnableProcessLock]
public async Task> NextElement(
[FromRoute] string org,
[FromRoute] string app,
@@ -269,6 +270,7 @@ public async Task> NextElement(
try
{
Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid);
+ await using var _ = await _processLocker.AcquireAsync(instance);
var processNextRequest = new ProcessNextRequest
{
diff --git a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs
index be3d2fac61..c3e5ed5819 100644
--- a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs
+++ b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs
@@ -28,7 +28,6 @@ public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicat
app.UseAuthorization();
app.UseTelemetryEnricher();
app.UseScopeAuthorization();
- app.UseMiddleware();
app.UseEndpoints(endpoints =>
{
diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs b/src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs
deleted file mode 100644
index 669148f47d..0000000000
--- a/src/Altinn.App.Api/Infrastructure/Middleware/EnableProcessLockAttribute.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Altinn.App.Api.Infrastructure.Middleware;
-
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
-internal sealed class EnableProcessLockAttribute : Attribute;
diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs
deleted file mode 100644
index a5bc61b81e..0000000000
--- a/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockMiddleware.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using Altinn.App.Core.Helpers;
-using Altinn.App.Core.Infrastructure.Clients.Storage;
-using Microsoft.Extensions.Options;
-
-namespace Altinn.App.Api.Infrastructure.Middleware;
-
-///
-/// Middleware that ensures only one request can proceed at a time by acquiring a lock.
-/// Only applies to endpoints decorated with .
-///
-internal sealed partial class ProcessLockMiddleware
-{
- private readonly RequestDelegate _next;
- private readonly ILogger _logger;
- private readonly ProcessLockClient _processClient;
- private readonly ProcessLockOptions _options;
-
- public ProcessLockMiddleware(
- RequestDelegate next,
- ILogger logger,
- IOptions options,
- ProcessLockClient processClient
- )
- {
- _next = next;
- _logger = logger;
- _processClient = processClient;
- _options = options.Value;
- }
-
- public async Task Invoke(HttpContext context)
- {
- var endpoint = context.GetEndpoint();
-
- if (endpoint?.Metadata.GetMetadata() is null)
- {
- await _next(context);
- return;
- }
-
- var (instanceOwnerPartyId, instanceGuid) =
- GetInstanceIdentifiers(context)
- ?? throw new InvalidOperationException("Unable to extract instance identifiers.");
- Guid? lockId = null;
-
- try
- {
- try
- {
- lockId = await _processClient.AcquireProcessLock(
- instanceGuid,
- instanceOwnerPartyId,
- _options.Expiration
- );
-
- LogLockAcquired(_logger, lockId.Value);
- }
- catch (PlatformHttpException e)
- {
- LogLockAcquisitionFailed(_logger);
- var problem = TypedResults.Problem(
- detail: e.Message,
- statusCode: e.Response.IsSuccessStatusCode ? 500 : (int)e.Response.StatusCode,
- title: "Failed to acquire lock."
- );
-
- await problem.ExecuteAsync(context);
-
- return;
- }
-
- await _next(context);
- }
- finally
- {
- if (lockId is not null)
- {
- try
- {
- await _processClient.ReleaseProcessLock(instanceGuid, instanceOwnerPartyId, lockId.Value);
-
- LogLockReleased(_logger, lockId.Value);
- }
- catch (Exception e)
- {
- LogLockReleaseFailed(_logger, lockId.Value, e);
- }
- }
- }
- }
-
- private static (int instanceOwnerPartyId, Guid instanceGuid)? GetInstanceIdentifiers(HttpContext context)
- {
- var routeData = context.GetRouteData();
-
- if (
- routeData.Values.TryGetValue("instanceOwnerPartyId", out var partyIdObj)
- && routeData.Values.TryGetValue("instanceGuid", out var guidObj)
- && int.TryParse(partyIdObj?.ToString(), out var partyId)
- && Guid.TryParse(guidObj?.ToString(), out var guid)
- )
- {
- return (partyId, guid);
- }
-
- return null;
- }
-
- [LoggerMessage(1, LogLevel.Debug, "Failed to acquire process lock.")]
- private static partial void LogLockAcquisitionFailed(ILogger logger);
-
- [LoggerMessage(2, LogLevel.Debug, "Acquired process lock with id: {LockId}")]
- private static partial void LogLockAcquired(ILogger logger, Guid lockId);
-
- [LoggerMessage(3, LogLevel.Debug, "Released process lock with id: {LockId}")]
- private static partial void LogLockReleased(ILogger logger, Guid lockId);
-
- [LoggerMessage(4, LogLevel.Error, "Failed to release process lock with id: {LockId}")]
- private static partial void LogLockReleaseFailed(ILogger logger, Guid lockId, Exception e);
-}
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index 20ad641c19..a67934b076 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -46,6 +46,7 @@
using Altinn.App.Core.Internal.Process.Authorization;
using Altinn.App.Core.Internal.Process.EventHandlers;
using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask;
+using Altinn.App.Core.Internal.Process.ProcessLock;
using Altinn.App.Core.Internal.Process.ProcessTasks;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy;
@@ -365,6 +366,8 @@ private static void AddProcessServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
+
// Process tasks
services.AddTransient();
services.AddTransient();
diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs
similarity index 67%
rename from src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs
rename to src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs
index 94b9d3a218..f3a0d839ed 100644
--- a/src/Altinn.App.Api/Infrastructure/Middleware/ProcessLockOptions.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs
@@ -1,4 +1,4 @@
-namespace Altinn.App.Api.Infrastructure.Middleware;
+namespace Altinn.App.Core.Internal.Process.ProcessLock;
internal sealed class ProcessLockOptions
{
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
new file mode 100644
index 0000000000..1225a96914
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
@@ -0,0 +1,69 @@
+using Altinn.App.Core.Infrastructure.Clients.Storage;
+using Altinn.Platform.Storage.Interface.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Altinn.App.Core.Internal.Process.ProcessLock;
+
+internal sealed partial class ProcessLocker(
+ ProcessLockClient client,
+ IOptions options,
+ ILogger logger
+)
+{
+ public async Task AcquireAsync(Instance instance)
+ {
+ var instanceIdParts = instance.Id.Split('/');
+ if (
+ instanceIdParts.Length != 2
+ || !int.TryParse(instanceIdParts[0], out var instanceOwnerPartyId)
+ || !Guid.TryParse(instanceIdParts[1], out var instanceGuid)
+ )
+ {
+ throw new ArgumentException("Instance ID was not in the expected format.");
+ }
+
+ var lockId = await client.AcquireProcessLock(instanceGuid, instanceOwnerPartyId, options.Value.Expiration);
+
+ LogLockAcquired(logger, lockId);
+
+ return new ProcessLock(instanceGuid, instanceOwnerPartyId, lockId, client, logger);
+ }
+
+ private sealed partial class ProcessLock(
+ Guid instanceGuid,
+ int instanceOwnerPartyId,
+ Guid lockId,
+ ProcessLockClient client,
+ ILogger logger
+ ) : IAsyncDisposable
+ {
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await client.ReleaseProcessLock(instanceGuid, instanceOwnerPartyId, lockId);
+
+ LogLockReleased(logger, lockId);
+
+ return;
+ }
+ catch (Exception e)
+ {
+ LogLockReleaseFailed(logger, lockId, e);
+ }
+ }
+ }
+
+ [LoggerMessage(1, LogLevel.Debug, "Failed to acquire process lock.")]
+ private static partial void LogLockAcquisitionFailed(ILogger logger);
+
+ [LoggerMessage(2, LogLevel.Debug, "Acquired process lock with id: {LockId}")]
+ private static partial void LogLockAcquired(ILogger logger, Guid lockId);
+
+ [LoggerMessage(3, LogLevel.Debug, "Released process lock with id: {LockId}")]
+ private static partial void LogLockReleased(ILogger logger, Guid lockId);
+
+ [LoggerMessage(4, LogLevel.Error, "Failed to release process lock with id: {LockId}")]
+ private static partial void LogLockReleaseFailed(ILogger logger, Guid lockId, Exception e);
+}
diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj
index 098b999643..9b0ae996d7 100644
--- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj
+++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj
@@ -18,7 +18,6 @@
-
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt
deleted file mode 100644
index a44cbff789..0000000000
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.EmptyJsonResponseBody_ReturnsProblemDetails.verified.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- Response: {
- Status: 500 Internal Server Error,
- Content: {
- Headers: {
- Content-Type: application/problem+json
- },
- Value: {
- type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
- title: Failed to acquire lock.,
- status: 500,
- detail: The response from the lock acquisition endpoint was not expected.
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt
deleted file mode 100644
index ccd8a701a9..0000000000
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.MissingRouteParameters_ThrowsException.verified.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- Exception: {
- $type: InvalidOperationException,
- Type: InvalidOperationException,
- Message: Unable to extract instance identifiers.,
- StackTrace:
-at Altinn.App.Api.Infrastructure.Middleware.ProcessLockMiddleware.Invoke(HttpContext context)
---- End of stack trace from previous location ---
-at Microsoft.AspNetCore.TestHost.ClientHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
-at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
---- End of stack trace from previous location ---
-at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
- }
-}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt
deleted file mode 100644
index a44cbff789..0000000000
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.NullResponseBody_ReturnsProblemDetails.verified.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- Response: {
- Status: 500 Internal Server Error,
- Content: {
- Headers: {
- Content-Type: application/problem+json
- },
- Value: {
- type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
- title: Failed to acquire lock.,
- status: 500,
- detail: The response from the lock acquisition endpoint was not expected.
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
deleted file mode 100644
index a6b96dea8f..0000000000
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=Conflict.verified.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- Response: {
- Status: 409 Conflict,
- Content: {
- Headers: {
- Content-Type: application/problem+json
- },
- Value: {
- type: https://tools.ietf.org/html/rfc9110#section-15.5.10,
- title: Failed to acquire lock.,
- status: 409,
- detail: 409 Conflict
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
deleted file mode 100644
index 6dfddf8227..0000000000
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=InternalServerError.verified.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- Response: {
- Status: 500 Internal Server Error,
- Content: {
- Headers: {
- Content-Type: application/problem+json
- },
- Value: {
- type: https://tools.ietf.org/html/rfc9110#section-15.6.1,
- title: Failed to acquire lock.,
- status: 500,
- detail: 500 Internal Server Error
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
deleted file mode 100644
index 417802ed99..0000000000
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.StorageApiError_ReturnsCorrectStatusCode_storageStatusCode=NotFound.verified.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- Response: {
- Status: 404 Not Found,
- Content: {
- Headers: {
- Content-Type: application/problem+json
- },
- Value: {
- type: https://tools.ietf.org/html/rfc9110#section-15.5.5,
- title: Failed to acquire lock.,
- status: 404,
- detail: 404 Not Found
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
similarity index 100%
rename from test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
new file mode 100644
index 0000000000..a50062a95c
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -0,0 +1,31 @@
+{
+ Exception: {
+ $type: PlatformHttpResponseSnapshotException,
+ Type: PlatformHttpResponseSnapshotException,
+ StatusCode: 200,
+ ReasonPhrase: OK,
+ HttpVersion: 1.1,
+ Content: ,
+ ContentTruncated: false,
+ Response: {
+ Status: 200 OK,
+ Headers: {
+ Date: DateTime_1,
+ Server: Kestrel,
+ Transfer-Encoding: chunked
+ },
+ Content: {
+ Headers: {
+ Content-Type: application/json
+ },
+ Value:
+ }
+ },
+ Message: The response from the lock acquisition endpoint was not expected.,
+ StackTrace:
+at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
new file mode 100644
index 0000000000..3660ab9708
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
@@ -0,0 +1,11 @@
+{
+ Exception: {
+ $type: ArgumentException,
+ Type: ArgumentException,
+ Message: Instance ID was not in the expected format.,
+ StackTrace:
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
new file mode 100644
index 0000000000..a50062a95c
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -0,0 +1,31 @@
+{
+ Exception: {
+ $type: PlatformHttpResponseSnapshotException,
+ Type: PlatformHttpResponseSnapshotException,
+ StatusCode: 200,
+ ReasonPhrase: OK,
+ HttpVersion: 1.1,
+ Content: ,
+ ContentTruncated: false,
+ Response: {
+ Status: 200 OK,
+ Headers: {
+ Date: DateTime_1,
+ Server: Kestrel,
+ Transfer-Encoding: chunked
+ },
+ Content: {
+ Headers: {
+ Content-Type: application/json
+ },
+ Value:
+ }
+ },
+ Message: The response from the lock acquisition endpoint was not expected.,
+ StackTrace:
+at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
new file mode 100644
index 0000000000..4f16fd3971
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
@@ -0,0 +1,28 @@
+{
+ Exception: {
+ $type: PlatformHttpResponseSnapshotException,
+ Type: PlatformHttpResponseSnapshotException,
+ StatusCode: 409,
+ ReasonPhrase: Conflict,
+ HttpVersion: 1.1,
+ Content: ,
+ ContentTruncated: false,
+ Response: {
+ Status: 409 Conflict,
+ Headers: {
+ Date: DateTime_1,
+ Server: Kestrel
+ },
+ Content: {
+ Headers: {},
+ Value:
+ }
+ },
+ Message: 409 Conflict,
+ StackTrace:
+at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
new file mode 100644
index 0000000000..4a64528477
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
@@ -0,0 +1,28 @@
+{
+ Exception: {
+ $type: PlatformHttpResponseSnapshotException,
+ Type: PlatformHttpResponseSnapshotException,
+ StatusCode: 500,
+ ReasonPhrase: Internal Server Error,
+ HttpVersion: 1.1,
+ Content: ,
+ ContentTruncated: false,
+ Response: {
+ Status: 500 Internal Server Error,
+ Headers: {
+ Date: DateTime_1,
+ Server: Kestrel
+ },
+ Content: {
+ Headers: {},
+ Value:
+ }
+ },
+ Message: 500 Internal Server Error,
+ StackTrace:
+at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
new file mode 100644
index 0000000000..f10a9626ba
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
@@ -0,0 +1,28 @@
+{
+ Exception: {
+ $type: PlatformHttpResponseSnapshotException,
+ Type: PlatformHttpResponseSnapshotException,
+ StatusCode: 404,
+ ReasonPhrase: Not Found,
+ HttpVersion: 1.1,
+ Content: ,
+ ContentTruncated: false,
+ Response: {
+ Status: 404 Not Found,
+ Headers: {
+ Date: DateTime_1,
+ Server: Kestrel
+ },
+ Content: {
+ Headers: {},
+ Value:
+ }
+ },
+ Message: 404 Not Found,
+ StackTrace:
+at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+--- End of stack trace from previous location ---
+at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
+ }
+}
\ No newline at end of file
diff --git a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
similarity index 51%
rename from test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
rename to test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
index a17988bc61..ef25e3c8f0 100644
--- a/test/Altinn.App.Api.Tests/Middleware/ProcessLockMiddlewareTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
@@ -1,108 +1,75 @@
using System.Net;
-using Altinn.App.Api.Infrastructure.Middleware;
using Altinn.App.Core.Configuration;
+using Altinn.App.Core.Helpers;
using Altinn.App.Core.Infrastructure.Clients.Storage;
using Altinn.App.Core.Internal.Process.ProcessLock;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
+using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
using WireMock.Matchers.Request;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
-namespace Altinn.App.Api.Tests.Middleware;
+namespace Altinn.App.Core.Tests.Internal.Process;
-public sealed class ProcessLockMiddlewareTests
+public sealed class ProcessLockTests
{
- private sealed record Fixture(IHost Host, WireMockServer Server) : IDisposable
+ private sealed record Fixture(WireMockServer Server, ServiceProvider ServiceProvider) : IDisposable
{
public readonly Guid InstanceGuid = Guid.NewGuid();
public readonly int InstanceOwnerPartyId = 12345;
private const string RuntimeCookieName = "test-cookie";
private const string BearerToken = "test-token";
- public static Fixture Create(Action? registerCustomAppServices = null)
+ public readonly string ServerUrl = Server.Url ?? throw new Exception("Missing server URL");
+
+ public static Fixture Create(Action? registerCustomServices = null)
{
var server = WireMockServer.Start();
- var host = new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
- .UseTestServer()
- .ConfigureServices(services =>
- {
- services.AddRouting();
-
- services.Configure(settings =>
- {
- var testUrl = server.Url ?? throw new Exception("Missing server URL");
- settings.ApiStorageEndpoint =
- testUrl + new Uri(settings.ApiStorageEndpoint).PathAndQuery;
- });
-
- services.Configure(settings => settings.RuntimeCookieName = RuntimeCookieName);
-
- services.AddHttpClient();
-
- services.AddHttpContextAccessor();
-
- registerCustomAppServices?.Invoke(services);
- })
- .Configure(app =>
- {
- app.UseRouting();
- app.UseMiddleware();
- app.UseEndpoints(endpoints =>
- {
- // Endpoint with lock
- endpoints
- .MapGet(
- "/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/test",
- () => "Success"
- )
- .WithMetadata(new EnableProcessLockAttribute());
-
- // Endpoint without lock
- endpoints.MapGet("/without-lock", () => "No lock required");
-
- // Endpoint with lock that throws exception
- endpoints
- .MapGet(
- "/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/test-exception",
- _ => throw new InvalidOperationException("Test exception")
- )
- .WithMetadata(new EnableProcessLockAttribute());
-
- // Endpoint with lock but no route parameters
- endpoints
- .MapGet("/invalid-route", () => Results.Ok())
- .WithMetadata(new EnableProcessLockAttribute());
- });
- });
- })
- .Start();
-
- return new Fixture(host, server);
+ var services = new ServiceCollection();
+
+ services.Configure(settings =>
+ {
+ var testUrl = server.Url ?? throw new Exception("Missing server URL");
+ settings.ApiStorageEndpoint = testUrl + new Uri(settings.ApiStorageEndpoint).PathAndQuery;
+ });
+
+ services.Configure(settings => settings.RuntimeCookieName = RuntimeCookieName);
+
+ services.AddHttpClient();
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Headers.Cookie = $"{RuntimeCookieName}={BearerToken}";
+
+ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
+ services.AddSingleton(httpContextAccessor);
+
+ services.AddTransient();
+
+ registerCustomServices?.Invoke(services);
+
+ var serviceProvider = services.BuildServiceProvider();
+
+ return new Fixture(server, serviceProvider);
}
public void Dispose()
{
Server.Stop();
Server.Dispose();
- Host.Dispose();
+ ServiceProvider.Dispose();
}
- public HttpClient GetTestClient()
+ public ProcessLocker GetProcessLocker()
{
- var httpClient = Host.GetTestClient();
- httpClient.DefaultRequestHeaders.Add("cookie", $"{RuntimeCookieName}={BearerToken}");
+ return ServiceProvider.GetRequiredService();
+ }
- return httpClient;
+ public Instance CreateInstance()
+ {
+ return new Instance { Id = $"{InstanceOwnerPartyId}/{InstanceGuid}" };
}
public IRequestBuilder GetAcquireLockRequestBuilder()
@@ -134,6 +101,8 @@ public async Task HappyPath()
var acquireLockRequestBuilder = fixture.GetAcquireLockRequestBuilder();
var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockId);
+ var testRequestBuilder = Request.Create().WithPath($"/test").UsingGet();
+
fixture
.Server.Given(acquireLockRequestBuilder)
.RespondWith(
@@ -147,41 +116,35 @@ public async Task HappyPath()
.Server.Given(releaseLockRequestBuilder)
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
- var response = await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+ fixture.Server.Given(testRequestBuilder).RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var httpClient = fixture.ServiceProvider.GetRequiredService().CreateClient();
- var content = await response.Content.ReadAsStringAsync();
- Assert.Equal("Success", content);
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
+
+ await using (var _ = await processLocker.AcquireAsync(instance))
+ {
+ using var response = await httpClient.GetAsync($"{fixture.ServerUrl}/test");
+ response.EnsureSuccessStatusCode();
+ }
var requests = fixture.Server.LogEntries;
- Assert.Equal(2, requests.Count);
+ Assert.Equal(3, requests.Count);
var acquireMatchResult = new RequestMatchResult();
acquireLockRequestBuilder.GetMatchingScore(requests[0].RequestMessage, acquireMatchResult);
Assert.True(acquireMatchResult.IsPerfectMatch);
+ var testMatchResult = new RequestMatchResult();
+ testRequestBuilder.GetMatchingScore(requests[1].RequestMessage, testMatchResult);
+ Assert.True(testMatchResult.IsPerfectMatch);
+
var releaseMatchResult = new RequestMatchResult();
- releaseLockRequestBuilder.GetMatchingScore(requests[1].RequestMessage, releaseMatchResult);
+ releaseLockRequestBuilder.GetMatchingScore(requests[2].RequestMessage, releaseMatchResult);
Assert.True(releaseMatchResult.IsPerfectMatch);
}
- [Fact]
- public async Task EndpointWithoutAttribute_SkipsMiddleware()
- {
- using var fixture = Fixture.Create();
-
- var response = await fixture.GetTestClient().GetAsync("/without-lock");
-
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var content = await response.Content.ReadAsStringAsync();
- Assert.Equal("No lock required", content);
-
- Assert.Empty(fixture.Server.LogEntries);
- }
-
[Fact]
public async Task LockReleasedOnException()
{
@@ -202,11 +165,14 @@ public async Task LockReleasedOnException()
.Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
- await Assert.ThrowsAsync(async () =>
- await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test-exception")
- );
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
+
+ await Assert.ThrowsAsync(async () =>
+ {
+ await using var _ = await processLocker.AcquireAsync(instance);
+ throw new Exception();
+ });
var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
Assert.Single(releaseRequests);
@@ -237,11 +203,10 @@ public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
.Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
- var response = await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ await using (var _ = await processLocker.AcquireAsync(instance)) { }
var acquireRequests = fixture.Server.FindLogEntries(fixture.GetAcquireLockRequestBuilder());
Assert.Single(acquireRequests);
@@ -251,20 +216,7 @@ public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
}
[Fact]
- public async Task MissingRouteParameters_ThrowsException()
- {
- using var fixture = Fixture.Create();
-
- var exception = await Assert.ThrowsAsync(async () =>
- await fixture.GetTestClient().GetAsync("/invalid-route")
- );
-
- Assert.Empty(fixture.Server.LogEntries);
- await Verify(new { Exception = exception });
- }
-
- [Fact]
- public async Task LockReleaseFailure_DoesNotAffectResponse()
+ public async Task LockReleaseFailure_DoesNotThrow()
{
using var fixture = Fixture.Create();
@@ -283,13 +235,10 @@ public async Task LockReleaseFailure_DoesNotAffectResponse()
.Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.InternalServerError));
- var response = await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var content = await response.Content.ReadAsStringAsync();
- Assert.Equal("Success", content);
+ await using (var _ = await processLocker.AcquireAsync(instance)) { }
var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
Assert.Single(releaseRequests);
@@ -299,7 +248,7 @@ public async Task LockReleaseFailure_DoesNotAffectResponse()
[InlineData(HttpStatusCode.Conflict)]
[InlineData(HttpStatusCode.NotFound)]
[InlineData(HttpStatusCode.InternalServerError)]
- public async Task StorageApiError_ReturnsCorrectStatusCode(HttpStatusCode storageStatusCode)
+ public async Task StorageApiError_ThrowsCorrectPlatformHttpException(HttpStatusCode storageStatusCode)
{
using var fixture = Fixture.Create();
@@ -307,17 +256,23 @@ public async Task StorageApiError_ReturnsCorrectStatusCode(HttpStatusCode storag
.Server.Given(fixture.GetAcquireLockRequestBuilder())
.RespondWith(Response.Create().WithStatusCode(storageStatusCode));
- var response = await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await using var _ = await processLocker.AcquireAsync(instance);
+ });
Assert.Single(fixture.Server.LogEntries);
- await Verify(new { Response = response }).UseParameters(storageStatusCode);
+ await Verify(new { Exception = exception })
+ .UseParameters(storageStatusCode)
+ .IgnoreMember(x => x.Headers);
}
[Fact]
- public async Task NullResponseBody_ReturnsProblemDetails()
+ public async Task NullResponseBody_ThrowsPlatformHttpException()
{
using var fixture = Fixture.Create();
@@ -331,17 +286,21 @@ public async Task NullResponseBody_ReturnsProblemDetails()
.WithBody("null")
);
- var response = await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await using var _ = await processLocker.AcquireAsync(instance);
+ });
Assert.Single(fixture.Server.LogEntries);
- await Verify(new { Response = response });
+ await Verify(new { Exception = exception }).IgnoreMember(x => x.Headers);
}
[Fact]
- public async Task EmptyJsonResponseBody_ReturnsProblemDetails()
+ public async Task EmptyJsonResponseBody_ThrowsPlatformHttpException()
{
using var fixture = Fixture.Create();
@@ -355,12 +314,33 @@ public async Task EmptyJsonResponseBody_ReturnsProblemDetails()
.WithBody("{}")
);
- var response = await fixture
- .GetTestClient()
- .GetAsync($"/instances/{fixture.InstanceOwnerPartyId}/{fixture.InstanceGuid}/test");
+ var processLocker = fixture.GetProcessLocker();
+ var instance = fixture.CreateInstance();
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await using var _ = await processLocker.AcquireAsync(instance);
+ });
Assert.Single(fixture.Server.LogEntries);
- await Verify(new { Response = response });
+ await Verify(new { Exception = exception }).IgnoreMember(x => x.Headers);
+ }
+
+ [Fact]
+ public async Task InvalidInstanceId_ThrowsArgumentException()
+ {
+ using var fixture = Fixture.Create();
+
+ var processLocker = fixture.GetProcessLocker();
+ var instance = new Instance { Id = "invalid-format" };
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await using var _ = await processLocker.AcquireAsync(instance);
+ });
+
+ Assert.Empty(fixture.Server.LogEntries);
+ await Verify(new { Exception = exception }).IgnoreMember(x => x.Headers);
}
}
From a9263b59be633b201a097ba3307373c6e3dd0885 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Wed, 10 Dec 2025 18:53:32 +0100
Subject: [PATCH 10/17] Add scoped service
---
.../Controllers/ProcessController.cs | 3 -
.../Extensions/ServiceCollectionExtensions.cs | 2 +-
.../Internal/Process/ProcessEngine.cs | 5 +
.../Process/ProcessLock/ProcessLocker.cs | 84 ++++++----
...y_ThrowsPlatformHttpException.verified.txt | 3 +-
...nceId_ThrowsArgumentException.verified.txt | 9 +-
...y_ThrowsPlatformHttpException.verified.txt | 3 +-
...on_storageStatusCode=Conflict.verified.txt | 3 +-
...tatusCode=InternalServerError.verified.txt | 3 +-
...on_storageStatusCode=NotFound.verified.txt | 3 +-
.../Internal/Process/ProcessLockTests.cs | 147 ++++++++++++------
11 files changed, 171 insertions(+), 94 deletions(-)
diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs
index 7684d58fcb..fd072491fb 100644
--- a/src/Altinn.App.Api/Controllers/ProcessController.cs
+++ b/src/Altinn.App.Api/Controllers/ProcessController.cs
@@ -41,7 +41,6 @@ public class ProcessController : ControllerBase
private readonly IProcessEngineAuthorizer _processEngineAuthorizer;
private readonly IValidationService _validationService;
private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer;
- private readonly ProcessLocker _processLocker;
///
/// Initializes a new instance of the
@@ -67,7 +66,6 @@ IProcessEngineAuthorizer processEngineAuthorizer
_processEngineAuthorizer = processEngineAuthorizer;
_validationService = validationService;
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService();
- _processLocker = serviceProvider.GetRequiredService();
}
///
@@ -270,7 +268,6 @@ public async Task> NextElement(
try
{
Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid);
- await using var _ = await _processLocker.AcquireAsync(instance);
var processNextRequest = new ProcessNextRequest
{
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index a67934b076..ea4469d621 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -366,7 +366,7 @@ private static void AddProcessServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
- services.AddTransient();
+ services.AddScoped();
// Process tasks
services.AddTransient();
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
index 3c84167766..4bc09f7093 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
@@ -9,6 +9,7 @@
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Process.Elements;
using Altinn.App.Core.Internal.Process.Elements.Base;
+using Altinn.App.Core.Internal.Process.ProcessLock;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks;
using Altinn.App.Core.Internal.Validation;
using Altinn.App.Core.Models;
@@ -42,6 +43,7 @@ public class ProcessEngine : IProcessEngine
private readonly ILogger _logger;
private readonly IValidationService _validationService;
private readonly IInstanceClient _instanceClient;
+ private readonly ProcessLocker _processLocker;
///
/// Initializes a new instance of the class.
@@ -74,6 +76,7 @@ public ProcessEngine(
_logger = logger;
_appImplementationFactory = serviceProvider.GetRequiredService();
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService();
+ _processLocker = serviceProvider.GetRequiredService();
}
///
@@ -252,6 +255,8 @@ out ProcessChangeResult? invalidProcessStateError
};
}
+ await _processLocker.LockAsync();
+
_logger.LogDebug(
"User successfully authorized to perform process next. Task ID: {CurrentTaskId}. Task type: {AltinnTaskType}. Action: {ProcessNextAction}.",
LogSanitizer.Sanitize(currentTaskId),
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
index 1225a96914..786036a453 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
@@ -1,5 +1,5 @@
using Altinn.App.Core.Infrastructure.Clients.Storage;
-using Altinn.Platform.Storage.Interface.Models;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -8,53 +8,73 @@ namespace Altinn.App.Core.Internal.Process.ProcessLock;
internal sealed partial class ProcessLocker(
ProcessLockClient client,
IOptions options,
- ILogger logger
-)
+ ILogger logger,
+ IHttpContextAccessor httpContextAccessor
+) : IAsyncDisposable
{
- public async Task AcquireAsync(Instance instance)
+ private readonly HttpContext _httpContext =
+ httpContextAccessor.HttpContext ?? throw new InvalidOperationException("HttpContext cannot be null.");
+
+ private ProcessLock? _lock;
+
+ public async ValueTask LockAsync()
{
- var instanceIdParts = instance.Id.Split('/');
- if (
- instanceIdParts.Length != 2
- || !int.TryParse(instanceIdParts[0], out var instanceOwnerPartyId)
- || !Guid.TryParse(instanceIdParts[1], out var instanceGuid)
- )
+ if (_lock is not null)
{
- throw new ArgumentException("Instance ID was not in the expected format.");
+ return;
}
+ var (instanceOwnerPartyId, instanceGuid) =
+ GetInstanceIdentifiers() ?? throw new InvalidOperationException("Unable to extract instance identifiers.");
+
var lockId = await client.AcquireProcessLock(instanceGuid, instanceOwnerPartyId, options.Value.Expiration);
LogLockAcquired(logger, lockId);
- return new ProcessLock(instanceGuid, instanceOwnerPartyId, lockId, client, logger);
+ _lock = new ProcessLock(instanceGuid, instanceOwnerPartyId, lockId);
+ }
+
+ private (int instanceOwnerPartyId, Guid instanceGuid)? GetInstanceIdentifiers()
+ {
+ var routeValues = _httpContext.Request.RouteValues;
+
+ if (
+ routeValues.TryGetValue("instanceOwnerPartyId", out var partyIdObj)
+ && routeValues.TryGetValue("instanceGuid", out var guidObj)
+ && int.TryParse(partyIdObj?.ToString(), out var partyId)
+ && Guid.TryParse(guidObj?.ToString(), out var guid)
+ )
+ {
+ return (partyId, guid);
+ }
+
+ return null;
}
- private sealed partial class ProcessLock(
- Guid instanceGuid,
- int instanceOwnerPartyId,
- Guid lockId,
- ProcessLockClient client,
- ILogger logger
- ) : IAsyncDisposable
+ public async ValueTask DisposeAsync()
{
- public async ValueTask DisposeAsync()
+ if (_lock is null)
+ {
+ return;
+ }
+
+ try
{
- try
- {
- await client.ReleaseProcessLock(instanceGuid, instanceOwnerPartyId, lockId);
-
- LogLockReleased(logger, lockId);
-
- return;
- }
- catch (Exception e)
- {
- LogLockReleaseFailed(logger, lockId, e);
- }
+ await client.ReleaseProcessLock(_lock.InstanceGuid, _lock.InstanceOwnerPartyId, _lock.LockId);
}
+ catch (Exception e)
+ {
+ LogLockReleaseFailed(logger, _lock.LockId, e);
+ return;
+ }
+
+ LogLockReleased(logger, _lock.LockId);
+
+ _lock = null;
}
+ private sealed record ProcessLock(Guid InstanceGuid, int InstanceOwnerPartyId, Guid LockId);
+
[LoggerMessage(1, LogLevel.Debug, "Failed to acquire process lock.")]
private static partial void LogLockAcquisitionFailed(ILogger logger);
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
index a50062a95c..c600555b06 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -24,7 +24,8 @@
Message: The response from the lock acquisition endpoint was not expected.,
StackTrace:
at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
index 3660ab9708..7c21f3ce0d 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
@@ -1,10 +1,11 @@
{
Exception: {
- $type: ArgumentException,
- Type: ArgumentException,
- Message: Instance ID was not in the expected format.,
+ $type: InvalidOperationException,
+ Type: InvalidOperationException,
+ Message: Unable to extract instance identifiers.,
StackTrace:
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
index a50062a95c..c600555b06 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -24,7 +24,8 @@
Message: The response from the lock acquisition endpoint was not expected.,
StackTrace:
at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
index 4f16fd3971..29380de14b 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
@@ -21,7 +21,8 @@
Message: 409 Conflict,
StackTrace:
at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
index 4a64528477..38d3db457d 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
@@ -21,7 +21,8 @@
Message: 500 Internal Server Error,
StackTrace:
at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
index f10a9626ba..8bc705b034 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
@@ -21,7 +21,8 @@
Message: 404 Not Found,
StackTrace:
at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.AcquireAsync(Instance instance)
+at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
index ef25e3c8f0..3389c885d0 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
@@ -3,7 +3,6 @@
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Infrastructure.Clients.Storage;
using Altinn.App.Core.Internal.Process.ProcessLock;
-using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using WireMock.Matchers.Request;
@@ -17,8 +16,8 @@ public sealed class ProcessLockTests
{
private sealed record Fixture(WireMockServer Server, ServiceProvider ServiceProvider) : IDisposable
{
- public readonly Guid InstanceGuid = Guid.NewGuid();
- public readonly int InstanceOwnerPartyId = 12345;
+ private static readonly Guid _instanceGuid = Guid.NewGuid();
+ private const int InstanceOwnerPartyId = 12345;
private const string RuntimeCookieName = "test-cookie";
private const string BearerToken = "test-token";
@@ -42,6 +41,8 @@ public static Fixture Create(Action? registerCustomServices
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Cookie = $"{RuntimeCookieName}={BearerToken}";
+ httpContext.Request.RouteValues.Add("instanceOwnerPartyId", InstanceOwnerPartyId);
+ httpContext.Request.RouteValues.Add("instanceGuid", _instanceGuid);
var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
services.AddSingleton(httpContextAccessor);
@@ -62,21 +63,11 @@ public void Dispose()
ServiceProvider.Dispose();
}
- public ProcessLocker GetProcessLocker()
- {
- return ServiceProvider.GetRequiredService();
- }
-
- public Instance CreateInstance()
- {
- return new Instance { Id = $"{InstanceOwnerPartyId}/{InstanceGuid}" };
- }
-
public IRequestBuilder GetAcquireLockRequestBuilder()
{
return Request
.Create()
- .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{InstanceGuid}/process/lock")
+ .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/process/lock")
.UsingPost()
.WithHeader("Authorization", $"Bearer {BearerToken}");
}
@@ -85,7 +76,7 @@ public IRequestBuilder GetReleaseLockRequestBuilder(Guid lockId)
{
return Request
.Create()
- .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{InstanceGuid}/process/lock/{lockId}")
+ .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/process/lock/{lockId}")
.UsingPatch()
.WithHeader("Authorization", $"Bearer {BearerToken}");
}
@@ -120,11 +111,65 @@ public async Task HappyPath()
var httpClient = fixture.ServiceProvider.GetRequiredService().CreateClient();
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
+ await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
+ {
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
+ using var response = await httpClient.GetAsync($"{fixture.ServerUrl}/test");
+ response.EnsureSuccessStatusCode();
+ }
+
+ var requests = fixture.Server.LogEntries;
+ Assert.Equal(3, requests.Count);
+
+ var acquireMatchResult = new RequestMatchResult();
+ acquireLockRequestBuilder.GetMatchingScore(requests[0].RequestMessage, acquireMatchResult);
+ Assert.True(acquireMatchResult.IsPerfectMatch);
+
+ var testMatchResult = new RequestMatchResult();
+ testRequestBuilder.GetMatchingScore(requests[1].RequestMessage, testMatchResult);
+ Assert.True(testMatchResult.IsPerfectMatch);
+
+ var releaseMatchResult = new RequestMatchResult();
+ releaseLockRequestBuilder.GetMatchingScore(requests[2].RequestMessage, releaseMatchResult);
+ Assert.True(releaseMatchResult.IsPerfectMatch);
+ }
+
+ [Fact]
+ public async Task HappyPath_MultipleLockCalls()
+ {
+ using var fixture = Fixture.Create();
+
+ var lockId = Guid.NewGuid();
+
+ var acquireLockRequestBuilder = fixture.GetAcquireLockRequestBuilder();
+ var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockId);
+
+ var testRequestBuilder = Request.Create().WithPath($"/test").UsingGet();
+
+ fixture
+ .Server.Given(acquireLockRequestBuilder)
+ .RespondWith(
+ Response
+ .Create()
+ .WithStatusCode(HttpStatusCode.OK)
+ .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ );
+
+ fixture
+ .Server.Given(releaseLockRequestBuilder)
+ .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
+
+ fixture.Server.Given(testRequestBuilder).RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
+
+ var httpClient = fixture.ServiceProvider.GetRequiredService().CreateClient();
- await using (var _ = await processLocker.AcquireAsync(instance))
+ await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
{
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
+ await processLocker.LockAsync();
+ await processLocker.LockAsync();
using var response = await httpClient.GetAsync($"{fixture.ServerUrl}/test");
response.EnsureSuccessStatusCode();
}
@@ -165,12 +210,11 @@ public async Task LockReleasedOnException()
.Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
-
await Assert.ThrowsAsync(async () =>
{
- await using var _ = await processLocker.AcquireAsync(instance);
+ await using var scope = fixture.ServiceProvider.CreateAsyncScope();
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
throw new Exception();
});
@@ -203,10 +247,11 @@ public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
.Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
-
- await using (var _ = await processLocker.AcquireAsync(instance)) { }
+ await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
+ {
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
+ }
var acquireRequests = fixture.Server.FindLogEntries(fixture.GetAcquireLockRequestBuilder());
Assert.Single(acquireRequests);
@@ -235,10 +280,11 @@ public async Task LockReleaseFailure_DoesNotThrow()
.Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.InternalServerError));
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
-
- await using (var _ = await processLocker.AcquireAsync(instance)) { }
+ await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
+ {
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
+ }
var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
Assert.Single(releaseRequests);
@@ -256,12 +302,11 @@ public async Task StorageApiError_ThrowsCorrectPlatformHttpException(HttpStatusC
.Server.Given(fixture.GetAcquireLockRequestBuilder())
.RespondWith(Response.Create().WithStatusCode(storageStatusCode));
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
-
var exception = await Assert.ThrowsAsync(async () =>
{
- await using var _ = await processLocker.AcquireAsync(instance);
+ await using var scope = fixture.ServiceProvider.CreateAsyncScope();
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
});
Assert.Single(fixture.Server.LogEntries);
@@ -286,12 +331,11 @@ public async Task NullResponseBody_ThrowsPlatformHttpException()
.WithBody("null")
);
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
-
var exception = await Assert.ThrowsAsync(async () =>
{
- await using var _ = await processLocker.AcquireAsync(instance);
+ await using var scope = fixture.ServiceProvider.CreateAsyncScope();
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
});
Assert.Single(fixture.Server.LogEntries);
@@ -314,12 +358,11 @@ public async Task EmptyJsonResponseBody_ThrowsPlatformHttpException()
.WithBody("{}")
);
- var processLocker = fixture.GetProcessLocker();
- var instance = fixture.CreateInstance();
-
var exception = await Assert.ThrowsAsync(async () =>
{
- await using var _ = await processLocker.AcquireAsync(instance);
+ await using var scope = fixture.ServiceProvider.CreateAsyncScope();
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
});
Assert.Single(fixture.Server.LogEntries);
@@ -330,17 +373,23 @@ public async Task EmptyJsonResponseBody_ThrowsPlatformHttpException()
[Fact]
public async Task InvalidInstanceId_ThrowsArgumentException()
{
- using var fixture = Fixture.Create();
-
- var processLocker = fixture.GetProcessLocker();
- var instance = new Instance { Id = "invalid-format" };
+ using var fixture = Fixture.Create(services =>
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.RouteValues.Add("instanceOwnerPartyId", "invalid");
+ httpContext.Request.RouteValues.Add("instanceGuid", "format");
+ var httpContextAccessor = new HttpContextAccessor { HttpContext = new DefaultHttpContext() };
+ services.AddSingleton(httpContextAccessor);
+ });
- var exception = await Assert.ThrowsAsync(async () =>
+ var exception = await Assert.ThrowsAsync(async () =>
{
- await using var _ = await processLocker.AcquireAsync(instance);
+ await using var scope = fixture.ServiceProvider.CreateAsyncScope();
+ var processLocker = scope.ServiceProvider.GetRequiredService();
+ await processLocker.LockAsync();
});
Assert.Empty(fixture.Server.LogEntries);
- await Verify(new { Exception = exception }).IgnoreMember(x => x.Headers);
+ await Verify(new { Exception = exception });
}
}
From 53e358a9931eb64ccfa6d5137bc9d0d038272449 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Tue, 23 Dec 2025 14:09:53 +0100
Subject: [PATCH 11/17] Adapt to changes in storage
---
Directory.Packages.props | 2 +-
.../Controllers/ProcessController.cs | 1 -
.../Extensions/ServiceCollectionExtensions.cs | 6 +-
...ent.cs => Telemetry.InstanceLockClient.cs} | 8 +-
...essLockClient.cs => InstanceLockClient.cs} | 43 ++++----
.../InstanceLocker.cs} | 48 ++++-----
.../Internal/Process/ProcessEngine.cs | 8 +-
.../Process/ProcessLock/ProcessLockOptions.cs | 6 --
.../Process/ProcessLock/ProcessLockRequest.cs | 6 --
.../ProcessLock/ProcessLockResponse.cs | 6 --
...stomTtl_UsedInStorageApiCall.verified.txt} | 0
..._ThrowsPlatformHttpException.verified.txt} | 4 +-
...ceId_ThrowsArgumentException.verified.txt} | 2 +-
..._ThrowsPlatformHttpException.verified.txt} | 4 +-
...n_storageStatusCode=Conflict.verified.txt} | 4 +-
...atusCode=InternalServerError.verified.txt} | 4 +-
...n_storageStatusCode=NotFound.verified.txt} | 4 +-
...ocessLockTests.cs => InstanceLockTests.cs} | 101 ++++++++++--------
.../InstanceLockTests.cs} | 10 +-
.../services/WaitForReleaseProcessTaskEnd.cs | 6 +-
20 files changed, 128 insertions(+), 145 deletions(-)
rename src/Altinn.App.Core/Features/Telemetry/{Telemetry.ProcessLockClient.cs => Telemetry.InstanceLockClient.cs} (52%)
rename src/Altinn.App.Core/Infrastructure/Clients/Storage/{ProcessLockClient.cs => InstanceLockClient.cs} (67%)
rename src/Altinn.App.Core/Internal/{Process/ProcessLock/ProcessLocker.cs => InstanceLocking/InstanceLocker.cs} (54%)
delete mode 100644 src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs
delete mode 100644 src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
delete mode 100644 src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt => InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt} (100%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt => InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt} (75%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt => InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt} (79%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt => InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt} (75%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt => InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt} (72%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt => InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt} (73%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt => InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt} (72%)
rename test/Altinn.App.Core.Tests/Internal/Process/{ProcessLockTests.cs => InstanceLockTests.cs} (80%)
rename test/Altinn.App.Integration.Tests/{ProcessLock/ProcessLockTests.cs => InstanceLocking/InstanceLockTests.cs} (81%)
rename test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/{process-lock => instance-lock}/services/WaitForReleaseProcessTaskEnd.cs (90%)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index bd5ac7974b..fa576ba756 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs
index fd072491fb..af08623f2b 100644
--- a/src/Altinn.App.Api/Controllers/ProcessController.cs
+++ b/src/Altinn.App.Api/Controllers/ProcessController.cs
@@ -9,7 +9,6 @@
using Altinn.App.Core.Internal.Process;
using Altinn.App.Core.Internal.Process.Elements;
using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties;
-using Altinn.App.Core.Internal.Process.ProcessLock;
using Altinn.App.Core.Internal.Validation;
using Altinn.App.Core.Models.Process;
using Altinn.App.Core.Models.Validation;
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index ea4469d621..fac3615e04 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -38,6 +38,7 @@
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Events;
using Altinn.App.Core.Internal.Expressions;
+using Altinn.App.Core.Internal.InstanceLocking;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Language;
using Altinn.App.Core.Internal.Pdf;
@@ -46,7 +47,6 @@
using Altinn.App.Core.Internal.Process.Authorization;
using Altinn.App.Core.Internal.Process.EventHandlers;
using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask;
-using Altinn.App.Core.Internal.Process.ProcessLock;
using Altinn.App.Core.Internal.Process.ProcessTasks;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy;
@@ -115,7 +115,7 @@ IWebHostEnvironment env
services.AddHttpClient();
#pragma warning restore CS0618 // Type or member is obsolete
services.AddHttpClient();
- services.AddHttpClient();
+ services.AddHttpClient();
services.AddHttpClient();
services.AddHttpClient();
@@ -366,7 +366,7 @@ private static void AddProcessServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
- services.AddScoped();
+ services.AddScoped();
// Process tasks
services.AddTransient();
diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.InstanceLockClient.cs
similarity index 52%
rename from src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
rename to src/Altinn.App.Core/Features/Telemetry/Telemetry.InstanceLockClient.cs
index 6131a40c29..e3c7ff606f 100644
--- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ProcessLockClient.cs
+++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.InstanceLockClient.cs
@@ -4,18 +4,18 @@ namespace Altinn.App.Core.Features;
partial class Telemetry
{
- internal Activity? StartAcquireProcessLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
+ internal Activity? StartAcquireInstanceLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
{
- var activity = ActivitySource.StartActivity("AcquireProcessLock");
+ var activity = ActivitySource.StartActivity("AcquireInstanceLock");
activity?.SetInstanceId(instanceGuid);
activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId);
return activity;
}
- internal Activity? StartReleaseProcessLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
+ internal Activity? StartReleaseInstanceLockActivity(Guid instanceGuid, int instanceOwnerPartyId)
{
- var activity = ActivitySource.StartActivity("ReleaseProcessLock");
+ var activity = ActivitySource.StartActivity("ReleaseInstanceLock");
activity?.SetInstanceId(instanceGuid);
activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId);
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
similarity index 67%
rename from src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
rename to src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
index dc46850ce2..8adb46e138 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
@@ -5,7 +5,7 @@
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features;
using Altinn.App.Core.Helpers;
-using Altinn.App.Core.Internal.Process.ProcessLock;
+using Altinn.Platform.Storage.Interface.Models;
using AltinnCore.Authentication.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -13,18 +13,18 @@
namespace Altinn.App.Core.Infrastructure.Clients.Storage;
-internal sealed class ProcessLockClient
+internal sealed class InstanceLockClient
{
private readonly AppSettings _appSettings;
- private readonly ILogger _logger;
+ private readonly ILogger _logger;
private readonly HttpClient _client;
private readonly Telemetry? _telemetry;
private readonly IHttpContextAccessor _httpContextAccessor;
- public ProcessLockClient(
+ public InstanceLockClient(
IOptions platformSettings,
IOptions appSettings,
- ILogger logger,
+ ILogger logger,
IHttpContextAccessor httpContextAccessor,
HttpClient httpClient,
Telemetry? telemetry = null
@@ -40,16 +40,16 @@ public ProcessLockClient(
_telemetry = telemetry;
}
- public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerPartyId, TimeSpan expiration)
+ public async Task AcquireInstanceLock(Guid instanceGuid, int instanceOwnerPartyId, TimeSpan expiration)
{
- using var activity = _telemetry?.StartAcquireProcessLockActivity(instanceGuid, instanceOwnerPartyId);
- string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/lock";
+ using var activity = _telemetry?.StartAcquireInstanceLockActivity(instanceGuid, instanceOwnerPartyId);
+ string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/lock";
string token = JwtTokenUtil.GetTokenFromContext(
_httpContextAccessor.HttpContext,
_appSettings.RuntimeCookieName
);
- var request = new ProcessLockRequest { TtlSeconds = (int)expiration.TotalSeconds };
+ var request = new InstanceLockRequest { TtlSeconds = (int)expiration.TotalSeconds };
var content = JsonContent.Create(request);
using var response = await _client.PostAsync(token, apiUrl, content);
@@ -59,18 +59,18 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response);
}
- Guid? lockId = null;
+ string? lockToken = null;
try
{
- var lockResponse = await response.Content.ReadFromJsonAsync();
- lockId = lockResponse?.LockId;
+ var lockResponse = await response.Content.ReadFromJsonAsync();
+ lockToken = lockResponse?.LockToken;
}
catch (Exception e)
{
_logger.LogError(e, "Error reading response from the lock acquisition endpoint.");
}
- if (lockId is null || lockId.Value == Guid.Empty)
+ if (string.IsNullOrEmpty(lockToken))
{
throw PlatformHttpResponseSnapshotException.Create(
"The response from the lock acquisition endpoint was not expected.",
@@ -78,22 +78,17 @@ public async Task AcquireProcessLock(Guid instanceGuid, int instanceOwnerP
);
}
- return lockId.Value;
+ return lockToken;
}
- public async Task ReleaseProcessLock(Guid instanceGuid, int instanceOwnerPartyId, Guid lockId)
+ public async Task ReleaseInstanceLock(Guid instanceGuid, int instanceOwnerPartyId, string lockToken)
{
- using var activity = _telemetry?.StartReleaseProcessLockActivity(instanceGuid, instanceOwnerPartyId);
- string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/lock/{lockId}";
- string token = JwtTokenUtil.GetTokenFromContext(
- _httpContextAccessor.HttpContext,
- _appSettings.RuntimeCookieName
- );
-
- var request = new ProcessLockRequest { TtlSeconds = 0 };
+ using var activity = _telemetry?.StartReleaseInstanceLockActivity(instanceGuid, instanceOwnerPartyId);
+ string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/lock";
+ var request = new InstanceLockRequest { TtlSeconds = 0 };
var content = JsonContent.Create(request);
- using var response = await _client.PatchAsync(token, apiUrl, content);
+ using var response = await _client.PatchAsync(lockToken, apiUrl, content);
if (!response.IsSuccessStatusCode)
{
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs b/src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs
similarity index 54%
rename from src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
rename to src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs
index 786036a453..b7f553b564 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLocker.cs
+++ b/src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs
@@ -1,23 +1,26 @@
using Altinn.App.Core.Infrastructure.Clients.Storage;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-namespace Altinn.App.Core.Internal.Process.ProcessLock;
+namespace Altinn.App.Core.Internal.InstanceLocking;
-internal sealed partial class ProcessLocker(
- ProcessLockClient client,
- IOptions options,
- ILogger logger,
+internal sealed partial class InstanceLocker(
+ InstanceLockClient client,
+ ILogger logger,
IHttpContextAccessor httpContextAccessor
) : IAsyncDisposable
{
private readonly HttpContext _httpContext =
httpContextAccessor.HttpContext ?? throw new InvalidOperationException("HttpContext cannot be null.");
- private ProcessLock? _lock;
+ private InstanceLock? _lock;
- public async ValueTask LockAsync()
+ public ValueTask LockAsync()
+ {
+ return LockAsync(TimeSpan.FromMinutes(5));
+ }
+
+ public async ValueTask LockAsync(TimeSpan ttl)
{
if (_lock is not null)
{
@@ -27,11 +30,11 @@ public async ValueTask LockAsync()
var (instanceOwnerPartyId, instanceGuid) =
GetInstanceIdentifiers() ?? throw new InvalidOperationException("Unable to extract instance identifiers.");
- var lockId = await client.AcquireProcessLock(instanceGuid, instanceOwnerPartyId, options.Value.Expiration);
+ var lockToken = await client.AcquireInstanceLock(instanceGuid, instanceOwnerPartyId, ttl);
- LogLockAcquired(logger, lockId);
+ LogLockAcquired(logger, instanceGuid);
- _lock = new ProcessLock(instanceGuid, instanceOwnerPartyId, lockId);
+ _lock = new InstanceLock(instanceGuid, instanceOwnerPartyId, lockToken);
}
private (int instanceOwnerPartyId, Guid instanceGuid)? GetInstanceIdentifiers()
@@ -60,30 +63,27 @@ public async ValueTask DisposeAsync()
try
{
- await client.ReleaseProcessLock(_lock.InstanceGuid, _lock.InstanceOwnerPartyId, _lock.LockId);
+ await client.ReleaseInstanceLock(_lock.InstanceGuid, _lock.InstanceOwnerPartyId, _lock.LockToken);
}
catch (Exception e)
{
- LogLockReleaseFailed(logger, _lock.LockId, e);
+ LogLockReleaseFailed(logger, _lock.InstanceGuid, e);
return;
}
- LogLockReleased(logger, _lock.LockId);
+ LogLockReleased(logger, _lock.InstanceGuid);
_lock = null;
}
- private sealed record ProcessLock(Guid InstanceGuid, int InstanceOwnerPartyId, Guid LockId);
-
- [LoggerMessage(1, LogLevel.Debug, "Failed to acquire process lock.")]
- private static partial void LogLockAcquisitionFailed(ILogger logger);
+ private sealed record InstanceLock(Guid InstanceGuid, int InstanceOwnerPartyId, string LockToken);
- [LoggerMessage(2, LogLevel.Debug, "Acquired process lock with id: {LockId}")]
- private static partial void LogLockAcquired(ILogger logger, Guid lockId);
+ [LoggerMessage(1, LogLevel.Debug, "Acquired lock for instance {InstanceGuid}.")]
+ private static partial void LogLockAcquired(ILogger logger, Guid instanceGuid);
- [LoggerMessage(3, LogLevel.Debug, "Released process lock with id: {LockId}")]
- private static partial void LogLockReleased(ILogger logger, Guid lockId);
+ [LoggerMessage(2, LogLevel.Debug, "Released lock for instance {InstanceGuid}.")]
+ private static partial void LogLockReleased(ILogger logger, Guid instanceGuid);
- [LoggerMessage(4, LogLevel.Error, "Failed to release process lock with id: {LockId}")]
- private static partial void LogLockReleaseFailed(ILogger logger, Guid lockId, Exception e);
+ [LoggerMessage(3, LogLevel.Error, "Failed to release lock for instance {InstanceGuid}.")]
+ private static partial void LogLockReleaseFailed(ILogger logger, Guid instanceGuid, Exception e);
}
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
index 4bc09f7093..3610ba7bb8 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
@@ -6,10 +6,10 @@
using Altinn.App.Core.Features.Auth;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Internal.Data;
+using Altinn.App.Core.Internal.InstanceLocking;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Process.Elements;
using Altinn.App.Core.Internal.Process.Elements.Base;
-using Altinn.App.Core.Internal.Process.ProcessLock;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks;
using Altinn.App.Core.Internal.Validation;
using Altinn.App.Core.Models;
@@ -43,7 +43,7 @@ public class ProcessEngine : IProcessEngine
private readonly ILogger _logger;
private readonly IValidationService _validationService;
private readonly IInstanceClient _instanceClient;
- private readonly ProcessLocker _processLocker;
+ private readonly InstanceLocker _instanceLocker;
///
/// Initializes a new instance of the class.
@@ -76,7 +76,7 @@ public ProcessEngine(
_logger = logger;
_appImplementationFactory = serviceProvider.GetRequiredService();
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService();
- _processLocker = serviceProvider.GetRequiredService();
+ _instanceLocker = serviceProvider.GetRequiredService();
}
///
@@ -255,7 +255,7 @@ out ProcessChangeResult? invalidProcessStateError
};
}
- await _processLocker.LockAsync();
+ await _instanceLocker.LockAsync();
_logger.LogDebug(
"User successfully authorized to perform process next. Task ID: {CurrentTaskId}. Task type: {AltinnTaskType}. Action: {ProcessNextAction}.",
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs
deleted file mode 100644
index f3a0d839ed..0000000000
--- a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockOptions.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Altinn.App.Core.Internal.Process.ProcessLock;
-
-internal sealed class ProcessLockOptions
-{
- public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5);
-}
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
deleted file mode 100644
index 3d55e85753..0000000000
--- a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockRequest.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Altinn.App.Core.Internal.Process.ProcessLock;
-
-internal sealed class ProcessLockRequest
-{
- public int TtlSeconds { get; set; }
-}
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs b/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs
deleted file mode 100644
index 7f7f2d40cc..0000000000
--- a/src/Altinn.App.Core/Internal/Process/ProcessLock/ProcessLockResponse.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Altinn.App.Core.Internal.Process.ProcessLock;
-
-internal sealed class ProcessLockResponse
-{
- public Guid LockId { get; set; }
-}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt
similarity index 100%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.CustomExpirationConfiguration_UsedInStorageApiCall.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
similarity index 75%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
index c600555b06..a41c118c29 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -23,8 +23,8 @@
},
Message: The response from the lock acquisition endpoint was not expected.,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
similarity index 79%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
index 7c21f3ce0d..0b5490f66d 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
@@ -4,7 +4,7 @@
Type: InvalidOperationException,
Message: Unable to extract instance identifiers.,
StackTrace:
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
similarity index 75%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
index c600555b06..a41c118c29 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -23,8 +23,8 @@
},
Message: The response from the lock acquisition endpoint was not expected.,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
similarity index 72%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
index 29380de14b..586bf06af6 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
@@ -20,8 +20,8 @@
},
Message: 409 Conflict,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
similarity index 73%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
index 38d3db457d..2d59753cb5 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
@@ -20,8 +20,8 @@
},
Message: 500 Internal Server Error,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
similarity index 72%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
index 8bc705b034..eff961bd02 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
@@ -20,8 +20,8 @@
},
Message: 404 Not Found,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.ProcessLockClient.AcquireProcessLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
-at Altinn.App.Core.Internal.Process.ProcessLock.ProcessLocker.LockAsync()
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
at Xunit.Assert.RecordExceptionAsync(Func`1 testCode)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
similarity index 80%
rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
index 3389c885d0..f3a1871261 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessLockTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
@@ -2,7 +2,8 @@
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Infrastructure.Clients.Storage;
-using Altinn.App.Core.Internal.Process.ProcessLock;
+using Altinn.App.Core.Internal.InstanceLocking;
+using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using WireMock.Matchers.Request;
@@ -12,7 +13,7 @@
namespace Altinn.App.Core.Tests.Internal.Process;
-public sealed class ProcessLockTests
+public sealed class InstanceLockTests
{
private sealed record Fixture(WireMockServer Server, ServiceProvider ServiceProvider) : IDisposable
{
@@ -37,7 +38,7 @@ public static Fixture Create(Action? registerCustomServices
services.Configure(settings => settings.RuntimeCookieName = RuntimeCookieName);
- services.AddHttpClient();
+ services.AddHttpClient();
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Cookie = $"{RuntimeCookieName}={BearerToken}";
@@ -47,7 +48,7 @@ public static Fixture Create(Action? registerCustomServices
var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
services.AddSingleton(httpContextAccessor);
- services.AddTransient();
+ services.AddTransient();
registerCustomServices?.Invoke(services);
@@ -67,18 +68,18 @@ public IRequestBuilder GetAcquireLockRequestBuilder()
{
return Request
.Create()
- .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/process/lock")
+ .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/lock")
.UsingPost()
.WithHeader("Authorization", $"Bearer {BearerToken}");
}
- public IRequestBuilder GetReleaseLockRequestBuilder(Guid lockId)
+ public IRequestBuilder GetReleaseLockRequestBuilder(string lockToken)
{
return Request
.Create()
- .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/process/lock/{lockId}")
+ .WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/lock")
.UsingPatch()
- .WithHeader("Authorization", $"Bearer {BearerToken}");
+ .WithHeader("Authorization", $"Bearer {lockToken}");
}
}
@@ -88,9 +89,10 @@ public async Task HappyPath()
using var fixture = Fixture.Create();
var lockId = Guid.NewGuid();
+ var lockToken = GenerateLockToken(lockId);
var acquireLockRequestBuilder = fixture.GetAcquireLockRequestBuilder();
- var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockId);
+ var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockToken);
var testRequestBuilder = Request.Create().WithPath($"/test").UsingGet();
@@ -100,7 +102,7 @@ public async Task HappyPath()
Response
.Create()
.WithStatusCode(HttpStatusCode.OK)
- .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ .WithBodyAsJson(new InstanceLockResponse { LockToken = lockToken })
);
fixture
@@ -113,8 +115,8 @@ public async Task HappyPath()
await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
{
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
using var response = await httpClient.GetAsync($"{fixture.ServerUrl}/test");
response.EnsureSuccessStatusCode();
}
@@ -141,9 +143,10 @@ public async Task HappyPath_MultipleLockCalls()
using var fixture = Fixture.Create();
var lockId = Guid.NewGuid();
+ var lockToken = GenerateLockToken(lockId);
var acquireLockRequestBuilder = fixture.GetAcquireLockRequestBuilder();
- var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockId);
+ var releaseLockRequestBuilder = fixture.GetReleaseLockRequestBuilder(lockToken);
var testRequestBuilder = Request.Create().WithPath($"/test").UsingGet();
@@ -153,7 +156,7 @@ public async Task HappyPath_MultipleLockCalls()
Response
.Create()
.WithStatusCode(HttpStatusCode.OK)
- .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ .WithBodyAsJson(new InstanceLockResponse { LockToken = lockToken })
);
fixture
@@ -166,10 +169,10 @@ public async Task HappyPath_MultipleLockCalls()
await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
{
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
- await processLocker.LockAsync();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
+ await instanceLocker.LockAsync();
+ await instanceLocker.LockAsync();
using var response = await httpClient.GetAsync($"{fixture.ServerUrl}/test");
response.EnsureSuccessStatusCode();
}
@@ -196,6 +199,7 @@ public async Task LockReleasedOnException()
using var fixture = Fixture.Create();
var lockId = Guid.NewGuid();
+ var lockToken = GenerateLockToken(lockId);
fixture
.Server.Given(fixture.GetAcquireLockRequestBuilder())
@@ -203,36 +207,33 @@ public async Task LockReleasedOnException()
Response
.Create()
.WithStatusCode(HttpStatusCode.OK)
- .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ .WithBodyAsJson(new InstanceLockResponse { LockToken = lockToken })
);
fixture
- .Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
+ .Server.Given(fixture.GetReleaseLockRequestBuilder(lockToken))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
await Assert.ThrowsAsync(async () =>
{
await using var scope = fixture.ServiceProvider.CreateAsyncScope();
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
throw new Exception();
});
- var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
+ var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockToken));
Assert.Single(releaseRequests);
}
[Fact]
- public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
+ public async Task CustomTtl_UsedInStorageApiCall()
{
var lockId = Guid.NewGuid();
- const int customExpirationSeconds = 120;
+ var lockToken = GenerateLockToken(lockId);
+ var ttl = TimeSpan.FromSeconds(120);
- using var fixture = Fixture.Create(services =>
- services.Configure(options =>
- options.Expiration = TimeSpan.FromSeconds(customExpirationSeconds)
- )
- );
+ using var fixture = Fixture.Create();
fixture
.Server.Given(fixture.GetAcquireLockRequestBuilder())
@@ -240,17 +241,17 @@ public async Task CustomExpirationConfiguration_UsedInStorageApiCall()
Response
.Create()
.WithStatusCode(HttpStatusCode.OK)
- .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ .WithBodyAsJson(new InstanceLockResponse { LockToken = lockToken })
);
fixture
- .Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
+ .Server.Given(fixture.GetReleaseLockRequestBuilder(lockToken))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
{
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync(ttl);
}
var acquireRequests = fixture.Server.FindLogEntries(fixture.GetAcquireLockRequestBuilder());
@@ -266,6 +267,7 @@ public async Task LockReleaseFailure_DoesNotThrow()
using var fixture = Fixture.Create();
var lockId = Guid.NewGuid();
+ var lockToken = GenerateLockToken(lockId);
fixture
.Server.Given(fixture.GetAcquireLockRequestBuilder())
@@ -273,20 +275,20 @@ public async Task LockReleaseFailure_DoesNotThrow()
Response
.Create()
.WithStatusCode(HttpStatusCode.OK)
- .WithBodyAsJson(new ProcessLockResponse { LockId = lockId })
+ .WithBodyAsJson(new InstanceLockResponse { LockToken = lockToken })
);
fixture
- .Server.Given(fixture.GetReleaseLockRequestBuilder(lockId))
+ .Server.Given(fixture.GetReleaseLockRequestBuilder(lockToken))
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.InternalServerError));
await using (var scope = fixture.ServiceProvider.CreateAsyncScope())
{
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
}
- var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockId));
+ var releaseRequests = fixture.Server.FindLogEntries(fixture.GetReleaseLockRequestBuilder(lockToken));
Assert.Single(releaseRequests);
}
@@ -305,8 +307,8 @@ public async Task StorageApiError_ThrowsCorrectPlatformHttpException(HttpStatusC
var exception = await Assert.ThrowsAsync(async () =>
{
await using var scope = fixture.ServiceProvider.CreateAsyncScope();
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
});
Assert.Single(fixture.Server.LogEntries);
@@ -334,8 +336,8 @@ public async Task NullResponseBody_ThrowsPlatformHttpException()
var exception = await Assert.ThrowsAsync(async () =>
{
await using var scope = fixture.ServiceProvider.CreateAsyncScope();
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
});
Assert.Single(fixture.Server.LogEntries);
@@ -361,8 +363,8 @@ public async Task EmptyJsonResponseBody_ThrowsPlatformHttpException()
var exception = await Assert.ThrowsAsync(async () =>
{
await using var scope = fixture.ServiceProvider.CreateAsyncScope();
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
});
Assert.Single(fixture.Server.LogEntries);
@@ -385,11 +387,16 @@ public async Task InvalidInstanceId_ThrowsArgumentException()
var exception = await Assert.ThrowsAsync(async () =>
{
await using var scope = fixture.ServiceProvider.CreateAsyncScope();
- var processLocker = scope.ServiceProvider.GetRequiredService();
- await processLocker.LockAsync();
+ var instanceLocker = scope.ServiceProvider.GetRequiredService();
+ await instanceLocker.LockAsync();
});
Assert.Empty(fixture.Server.LogEntries);
await Verify(new { Exception = exception });
}
+
+ private string GenerateLockToken(Guid lockId)
+ {
+ return Convert.ToBase64String(lockId.ToByteArray());
+ }
}
diff --git a/test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs b/test/Altinn.App.Integration.Tests/InstanceLocking/InstanceLockTests.cs
similarity index 81%
rename from test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs
rename to test/Altinn.App.Integration.Tests/InstanceLocking/InstanceLockTests.cs
index c20f410f82..573f8f1728 100644
--- a/test/Altinn.App.Integration.Tests/ProcessLock/ProcessLockTests.cs
+++ b/test/Altinn.App.Integration.Tests/InstanceLocking/InstanceLockTests.cs
@@ -2,21 +2,21 @@
using Altinn.Platform.Storage.Interface.Models;
using Xunit.Abstractions;
-namespace Altinn.App.Integration.Tests.ProcessLock;
+namespace Altinn.App.Integration.Tests.InstanceLocking;
[Trait("Category", "Integration")]
-public sealed class ProcessLockTests(ITestOutputHelper _output, AppFixtureClassFixture _classFixture)
+public sealed class InstanceLockTests(ITestOutputHelper _output, AppFixtureClassFixture _classFixture)
: IClassFixture
{
[Fact]
public async Task ProcessNext_ConcurrentRequests_OneRequestGetsConflict()
{
- await using var fixtureScope = await _classFixture.Get(_output, TestApps.Basic, "process-lock");
+ await using var fixtureScope = await _classFixture.Get(_output, TestApps.Basic, "instance-lock");
var fixture = fixtureScope.Fixture;
var client = fixture.GetAppClient();
- var resetResponse = await client.PostAsync("/test/process-lock/reset", null);
+ var resetResponse = await client.PostAsync("/test/instance-lock/reset", null);
resetResponse.EnsureSuccessStatusCode();
var token = await fixture.Auth.GetUserToken(userId: 1337);
@@ -40,7 +40,7 @@ public async Task ProcessNext_ConcurrentRequests_OneRequestGetsConflict()
Assert.Equal(System.Net.HttpStatusCode.Conflict, conflictResponse.Response.StatusCode);
- var releaseResponse = await client.PostAsync("/test/process-lock/release-wait", null);
+ var releaseResponse = await client.PostAsync("/test/instance-lock/release-wait", null);
releaseResponse.EnsureSuccessStatusCode();
using var successRequestResponse = await processNextTasks.First(x => x != conflictResponseTask);
diff --git a/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs b/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/instance-lock/services/WaitForReleaseProcessTaskEnd.cs
similarity index 90%
rename from test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs
rename to test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/instance-lock/services/WaitForReleaseProcessTaskEnd.cs
index 1d6605269f..1210d8e2bc 100644
--- a/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/process-lock/services/WaitForReleaseProcessTaskEnd.cs
+++ b/test/Altinn.App.Integration.Tests/_testapps/basic/_scenarios/instance-lock/services/WaitForReleaseProcessTaskEnd.cs
@@ -8,7 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using TestApp.Shared;
-namespace Altinn.App.Integration.Tests.Scenarios.ProcessLock;
+namespace Altinn.App.Integration.Tests.Scenarios.InstanceLock;
public sealed class WaitForReleaseProcessTaskEnd : IProcessTaskEnd
{
@@ -36,7 +36,7 @@ public sealed class WaitForReleaseProcessTaskEndEndpoints : IEndpointConfigurato
public void ConfigureEndpoints(WebApplication app)
{
app.MapPost(
- "/test/process-lock/release-wait",
+ "/test/instance-lock/release-wait",
() =>
{
WaitForReleaseProcessTaskEnd.Release();
@@ -45,7 +45,7 @@ public void ConfigureEndpoints(WebApplication app)
);
app.MapPost(
- "/test/process-lock/reset",
+ "/test/instance-lock/reset",
() =>
{
WaitForReleaseProcessTaskEnd.Reset();
From f96ff3af32eef16a2bd4bc9dd79e6bad7e2e6114 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Tue, 23 Dec 2025 14:39:47 +0100
Subject: [PATCH 12/17] Correct test
---
...idInstanceId_ThrowsInvalidOperationException.verified.txt} | 0
.../Internal/Process/InstanceLockTests.cs | 4 ++--
2 files changed, 2 insertions(+), 2 deletions(-)
rename test/Altinn.App.Core.Tests/Internal/Process/{InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt => InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt} (100%)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt
similarity index 100%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsArgumentException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
index f3a1871261..ddd1c02767 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
@@ -373,14 +373,14 @@ public async Task EmptyJsonResponseBody_ThrowsPlatformHttpException()
}
[Fact]
- public async Task InvalidInstanceId_ThrowsArgumentException()
+ public async Task InvalidInstanceId_ThrowsInvalidOperationException()
{
using var fixture = Fixture.Create(services =>
{
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues.Add("instanceOwnerPartyId", "invalid");
httpContext.Request.RouteValues.Add("instanceGuid", "format");
- var httpContextAccessor = new HttpContextAccessor { HttpContext = new DefaultHttpContext() };
+ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
services.AddSingleton(httpContextAccessor);
});
From 12eabe082b86c0cda8f0be833622644406b78541 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Wed, 31 Dec 2025 11:31:58 +0100
Subject: [PATCH 13/17] Use IAuthenticationTokenResolver
---
.../Clients/Storage/InstanceLockClient.cs | 50 ++++++++++++-------
...ustomTtl_UsedInStorageApiCall.verified.txt | 0
...y_ThrowsPlatformHttpException.verified.txt | 2 +-
...rowsInvalidOperationException.verified.txt | 0
...y_ThrowsPlatformHttpException.verified.txt | 2 +-
...on_storageStatusCode=Conflict.verified.txt | 2 +-
...tatusCode=InternalServerError.verified.txt | 2 +-
...on_storageStatusCode=NotFound.verified.txt | 2 +-
.../InstanceLockTests.cs | 33 +++++++++---
9 files changed, 63 insertions(+), 30 deletions(-)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt (100%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt (89%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt (100%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt (89%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt (87%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt (88%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt (87%)
rename test/Altinn.App.Core.Tests/Internal/{Process => InstanceLocking}/InstanceLockTests.cs (91%)
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
index 8adb46e138..d7ea77721e 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
@@ -1,12 +1,13 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
+using System.Text.Json;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Constants;
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features;
using Altinn.App.Core.Helpers;
+using Altinn.App.Core.Internal.Auth;
using Altinn.Platform.Storage.Interface.Models;
-using AltinnCore.Authentication.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -15,24 +16,23 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage;
internal sealed class InstanceLockClient
{
- private readonly AppSettings _appSettings;
private readonly ILogger _logger;
private readonly HttpClient _client;
private readonly Telemetry? _telemetry;
- private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IAuthenticationTokenResolver _authenticationTokenResolver;
+
+ private readonly AuthenticationMethod _defaultAuthenticationMethod = StorageAuthenticationMethod.CurrentUser();
public InstanceLockClient(
IOptions platformSettings,
- IOptions appSettings,
ILogger logger,
- IHttpContextAccessor httpContextAccessor,
+ IAuthenticationTokenResolver authenticationTokenResolver,
HttpClient httpClient,
Telemetry? telemetry = null
)
{
- _appSettings = appSettings.Value;
_logger = logger;
- _httpContextAccessor = httpContextAccessor;
+ _authenticationTokenResolver = authenticationTokenResolver;
httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint);
httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
@@ -40,32 +40,41 @@ public InstanceLockClient(
_telemetry = telemetry;
}
- public async Task AcquireInstanceLock(Guid instanceGuid, int instanceOwnerPartyId, TimeSpan expiration)
+ public async Task AcquireInstanceLock(
+ Guid instanceGuid,
+ int instanceOwnerPartyId,
+ TimeSpan expiration,
+ StorageAuthenticationMethod? authenticationMethod = null,
+ CancellationToken cancellationToken = default
+ )
{
using var activity = _telemetry?.StartAcquireInstanceLockActivity(instanceGuid, instanceOwnerPartyId);
string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/lock";
- string token = JwtTokenUtil.GetTokenFromContext(
- _httpContextAccessor.HttpContext,
- _appSettings.RuntimeCookieName
+
+ var token = await _authenticationTokenResolver.GetAccessToken(
+ authenticationMethod ?? _defaultAuthenticationMethod,
+ cancellationToken: cancellationToken
);
var request = new InstanceLockRequest { TtlSeconds = (int)expiration.TotalSeconds };
var content = JsonContent.Create(request);
- using var response = await _client.PostAsync(token, apiUrl, content);
+ using var response = await _client.PostAsync(token, apiUrl, content, cancellationToken: cancellationToken);
if (!response.IsSuccessStatusCode)
{
- throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response);
+ throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response, cancellationToken);
}
string? lockToken = null;
try
{
- var lockResponse = await response.Content.ReadFromJsonAsync();
+ var lockResponse = await response.Content.ReadFromJsonAsync(
+ cancellationToken: cancellationToken
+ );
lockToken = lockResponse?.LockToken;
}
- catch (Exception e)
+ catch (Exception e) when (e is JsonException || e is InvalidOperationException)
{
_logger.LogError(e, "Error reading response from the lock acquisition endpoint.");
}
@@ -81,18 +90,23 @@ public async Task AcquireInstanceLock(Guid instanceGuid, int instanceOwn
return lockToken;
}
- public async Task ReleaseInstanceLock(Guid instanceGuid, int instanceOwnerPartyId, string lockToken)
+ public async Task ReleaseInstanceLock(
+ Guid instanceGuid,
+ int instanceOwnerPartyId,
+ string lockToken,
+ CancellationToken cancellationToken = default
+ )
{
using var activity = _telemetry?.StartReleaseInstanceLockActivity(instanceGuid, instanceOwnerPartyId);
string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/lock";
var request = new InstanceLockRequest { TtlSeconds = 0 };
var content = JsonContent.Create(request);
- using var response = await _client.PatchAsync(lockToken, apiUrl, content);
+ using var response = await _client.PatchAsync(lockToken, apiUrl, content, cancellationToken: cancellationToken);
if (!response.IsSuccessStatusCode)
{
- throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response);
+ throw await PlatformHttpResponseSnapshotException.CreateAndDisposeHttpResponse(response, cancellationToken);
}
}
}
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt
similarity index 100%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.CustomTtl_UsedInStorageApiCall.verified.txt
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
similarity index 89%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
index a41c118c29..0cc7635a1e 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.EmptyJsonResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -23,7 +23,7 @@
},
Message: The response from the lock acquisition endpoint was not expected.,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration, StorageAuthenticationMethod authenticationMethod, CancellationToken cancellationToken)
at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt
similarity index 100%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.InvalidInstanceId_ThrowsInvalidOperationException.verified.txt
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
similarity index 89%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
index a41c118c29..0cc7635a1e 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.NullResponseBody_ThrowsPlatformHttpException.verified.txt
@@ -23,7 +23,7 @@
},
Message: The response from the lock acquisition endpoint was not expected.,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration, StorageAuthenticationMethod authenticationMethod, CancellationToken cancellationToken)
at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
similarity index 87%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
index 586bf06af6..a220293cfa 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=Conflict.verified.txt
@@ -20,7 +20,7 @@
},
Message: 409 Conflict,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration, StorageAuthenticationMethod authenticationMethod, CancellationToken cancellationToken)
at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
similarity index 88%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
index 2d59753cb5..7793637439 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=InternalServerError.verified.txt
@@ -20,7 +20,7 @@
},
Message: 500 Internal Server Error,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration, StorageAuthenticationMethod authenticationMethod, CancellationToken cancellationToken)
at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
similarity index 87%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
index eff961bd02..5931ac0c2d 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.StorageApiError_ThrowsCorrectPlatformHttpException_storageStatusCode=NotFound.verified.txt
@@ -20,7 +20,7 @@
},
Message: 404 Not Found,
StackTrace:
-at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration)
+at Altinn.App.Core.Infrastructure.Clients.Storage.InstanceLockClient.AcquireInstanceLock(Guid instanceGuid, Int32 instanceOwnerPartyId, TimeSpan expiration, StorageAuthenticationMethod authenticationMethod, CancellationToken cancellationToken)
at Altinn.App.Core.Internal.InstanceLocking.InstanceLocker.LockAsync(TimeSpan ttl)
--- End of stack trace from previous location ---
--- End of stack trace from previous location ---
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
similarity index 91%
rename from test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
rename to test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
index ddd1c02767..fae1b4a232 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/InstanceLockTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
@@ -1,17 +1,23 @@
using System.Net;
using Altinn.App.Core.Configuration;
+using Altinn.App.Core.Features.Auth;
+using Altinn.App.Core.Features.Maskinporten;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Infrastructure.Clients.Storage;
+using Altinn.App.Core.Internal;
+using Altinn.App.Core.Internal.App;
+using Altinn.App.Core.Internal.Auth;
using Altinn.App.Core.Internal.InstanceLocking;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
+using Moq;
using WireMock.Matchers.Request;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
-namespace Altinn.App.Core.Tests.Internal.Process;
+namespace Altinn.App.Core.Tests.Internal.InstanceLocking;
public sealed class InstanceLockTests
{
@@ -19,8 +25,8 @@ private sealed record Fixture(WireMockServer Server, ServiceProvider ServiceProv
{
private static readonly Guid _instanceGuid = Guid.NewGuid();
private const int InstanceOwnerPartyId = 12345;
- private const string RuntimeCookieName = "test-cookie";
- private const string BearerToken = "test-token";
+
+ private static readonly Authenticated _defaultAuth = TestAuthentication.GetUserAuthentication();
public readonly string ServerUrl = Server.Url ?? throw new Exception("Missing server URL");
@@ -36,18 +42,24 @@ public static Fixture Create(Action? registerCustomServices
settings.ApiStorageEndpoint = testUrl + new Uri(settings.ApiStorageEndpoint).PathAndQuery;
});
- services.Configure(settings => settings.RuntimeCookieName = RuntimeCookieName);
+ var mocks = new FixtureMocks();
+ mocks.AuthenticationContextMock.Setup(x => x.Current).Returns(_defaultAuth);
services.AddHttpClient();
var httpContext = new DefaultHttpContext();
- httpContext.Request.Headers.Cookie = $"{RuntimeCookieName}={BearerToken}";
httpContext.Request.RouteValues.Add("instanceOwnerPartyId", InstanceOwnerPartyId);
httpContext.Request.RouteValues.Add("instanceGuid", _instanceGuid);
-
var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
services.AddSingleton(httpContextAccessor);
+ services.AddSingleton();
+ services.AddSingleton(mocks.MaskinportenClientMock.Object);
+ services.AddSingleton(mocks.AppMetadataMock.Object);
+ services.AddSingleton(mocks.AuthenticationContextMock.Object);
+
+ services.AddRuntimeEnvironment();
+
services.AddTransient();
registerCustomServices?.Invoke(services);
@@ -57,6 +69,13 @@ public static Fixture Create(Action? registerCustomServices
return new Fixture(server, serviceProvider);
}
+ public sealed record FixtureMocks
+ {
+ public Mock AuthenticationContextMock { get; init; } = new(MockBehavior.Strict);
+ public Mock AppMetadataMock { get; init; } = new(MockBehavior.Strict);
+ public Mock MaskinportenClientMock { get; init; } = new(MockBehavior.Strict);
+ }
+
public void Dispose()
{
Server.Stop();
@@ -70,7 +89,7 @@ public IRequestBuilder GetAcquireLockRequestBuilder()
.Create()
.WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/lock")
.UsingPost()
- .WithHeader("Authorization", $"Bearer {BearerToken}");
+ .WithHeader("Authorization", $"Bearer {_defaultAuth.Token}");
}
public IRequestBuilder GetReleaseLockRequestBuilder(string lockToken)
From 78d35f53d36d828ef0abae244a7c19240914d6be Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Wed, 31 Dec 2025 11:40:43 +0100
Subject: [PATCH 14/17] Use scoped service
---
.../Infrastructure/Clients/Storage/InstanceLockClient.cs | 1 -
.../Internal/InstanceLocking/InstanceLockTests.cs | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
index d7ea77721e..c7ceab87ed 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
@@ -8,7 +8,6 @@
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Internal.Auth;
using Altinn.Platform.Storage.Interface.Models;
-using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
diff --git a/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
index fae1b4a232..69b947e0b4 100644
--- a/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
@@ -60,7 +60,7 @@ public static Fixture Create(Action? registerCustomServices
services.AddRuntimeEnvironment();
- services.AddTransient();
+ services.AddScoped();
registerCustomServices?.Invoke(services);
From f36e20f4029f7729f8b70c2f64aa6dbf58b8c022 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Sun, 18 Jan 2026 11:54:45 +0100
Subject: [PATCH 15/17] Use custom header for lock token
---
.../Clients/Storage/InstanceLockClient.cs | 18 +++++++++++++++---
.../InstanceLocking/InstanceLockTests.cs | 3 ++-
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
index c7ceab87ed..127921e579 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceLockClient.cs
@@ -22,6 +22,8 @@ internal sealed class InstanceLockClient
private readonly AuthenticationMethod _defaultAuthenticationMethod = StorageAuthenticationMethod.CurrentUser();
+ private const string LockTokenHeaderName = "Altinn-Storage-Lock-Token";
+
public InstanceLockClient(
IOptions platformSettings,
ILogger logger,
@@ -93,15 +95,25 @@ public async Task ReleaseInstanceLock(
Guid instanceGuid,
int instanceOwnerPartyId,
string lockToken,
+ StorageAuthenticationMethod? authenticationMethod = null,
CancellationToken cancellationToken = default
)
{
using var activity = _telemetry?.StartReleaseInstanceLockActivity(instanceGuid, instanceOwnerPartyId);
string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/lock";
- var request = new InstanceLockRequest { TtlSeconds = 0 };
- var content = JsonContent.Create(request);
+ var instanceLockRequest = new InstanceLockRequest { TtlSeconds = 0 };
+
+ var userToken = await _authenticationTokenResolver.GetAccessToken(
+ authenticationMethod ?? _defaultAuthenticationMethod,
+ cancellationToken: cancellationToken
+ );
+
+ using HttpRequestMessage request = new(HttpMethod.Patch, apiUrl);
+ request.Content = JsonContent.Create(instanceLockRequest);
+ request.Headers.Authorization = new AuthenticationHeaderValue(AuthorizationSchemes.Bearer, userToken);
+ request.Headers.Add(LockTokenHeaderName, lockToken);
- using var response = await _client.PatchAsync(lockToken, apiUrl, content, cancellationToken: cancellationToken);
+ using var response = await _client.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
diff --git a/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
index 69b947e0b4..8a5f370e86 100644
--- a/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/InstanceLocking/InstanceLockTests.cs
@@ -98,7 +98,8 @@ public IRequestBuilder GetReleaseLockRequestBuilder(string lockToken)
.Create()
.WithPath($"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceGuid}/lock")
.UsingPatch()
- .WithHeader("Authorization", $"Bearer {lockToken}");
+ .WithHeader("Authorization", $"Bearer {_defaultAuth.Token}")
+ .WithHeader("Altinn-Storage-Lock-Token", lockToken);
}
}
From 9125ddbe9eaaea91a170c336ff65fb0352800d10 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Wed, 28 Jan 2026 12:28:29 +0100
Subject: [PATCH 16/17] Mock InstanceLocker in tests
---
.../Extensions/ServiceCollectionExtensions.cs | 2 +-
.../Internal/InstanceLocking/IInstanceLocker.cs | 8 ++++++++
.../Internal/InstanceLocking/InstanceLocker.cs | 2 +-
.../Internal/Process/ProcessEngine.cs | 4 ++--
.../Altinn.App.Api.Tests/Mocks/InstanceLockerMock.cs | 12 ++++++++++++
test/Altinn.App.Api.Tests/Program.cs | 2 ++
6 files changed, 26 insertions(+), 4 deletions(-)
create mode 100644 src/Altinn.App.Core/Internal/InstanceLocking/IInstanceLocker.cs
create mode 100644 test/Altinn.App.Api.Tests/Mocks/InstanceLockerMock.cs
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index 005d06b56e..1d981eff2e 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -370,7 +370,7 @@ private static void AddProcessServices(IServiceCollection services)
services.AddTransient();
services.AddTransient();
- services.AddScoped();
+ services.AddScoped();
// Process tasks
services.AddTransient();
diff --git a/src/Altinn.App.Core/Internal/InstanceLocking/IInstanceLocker.cs b/src/Altinn.App.Core/Internal/InstanceLocking/IInstanceLocker.cs
new file mode 100644
index 0000000000..6c7c814bb9
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/InstanceLocking/IInstanceLocker.cs
@@ -0,0 +1,8 @@
+namespace Altinn.App.Core.Internal.InstanceLocking;
+
+internal interface IInstanceLocker : IAsyncDisposable
+{
+ ValueTask LockAsync();
+
+ ValueTask LockAsync(TimeSpan ttl);
+}
diff --git a/src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs b/src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs
index b7f553b564..4be57623c9 100644
--- a/src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs
+++ b/src/Altinn.App.Core/Internal/InstanceLocking/InstanceLocker.cs
@@ -8,7 +8,7 @@ internal sealed partial class InstanceLocker(
InstanceLockClient client,
ILogger logger,
IHttpContextAccessor httpContextAccessor
-) : IAsyncDisposable
+) : IInstanceLocker
{
private readonly HttpContext _httpContext =
httpContextAccessor.HttpContext ?? throw new InvalidOperationException("HttpContext cannot be null.");
diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
index 3610ba7bb8..375bde1c83 100644
--- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
+++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
@@ -43,7 +43,7 @@ public class ProcessEngine : IProcessEngine
private readonly ILogger _logger;
private readonly IValidationService _validationService;
private readonly IInstanceClient _instanceClient;
- private readonly InstanceLocker _instanceLocker;
+ private readonly IInstanceLocker _instanceLocker;
///
/// Initializes a new instance of the class.
@@ -76,7 +76,7 @@ public ProcessEngine(
_logger = logger;
_appImplementationFactory = serviceProvider.GetRequiredService();
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService();
- _instanceLocker = serviceProvider.GetRequiredService();
+ _instanceLocker = serviceProvider.GetRequiredService();
}
///
diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceLockerMock.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceLockerMock.cs
new file mode 100644
index 0000000000..8748dc3a6a
--- /dev/null
+++ b/test/Altinn.App.Api.Tests/Mocks/InstanceLockerMock.cs
@@ -0,0 +1,12 @@
+using Altinn.App.Core.Internal.InstanceLocking;
+
+namespace Altinn.App.Api.Tests.Mocks;
+
+internal sealed class InstanceLockerMock : IInstanceLocker
+{
+ public ValueTask LockAsync() => ValueTask.CompletedTask;
+
+ public ValueTask LockAsync(TimeSpan ttl) => ValueTask.CompletedTask;
+
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
+}
diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs
index a2db05bc5d..e6e3de0daa 100644
--- a/test/Altinn.App.Api.Tests/Program.cs
+++ b/test/Altinn.App.Api.Tests/Program.cs
@@ -13,6 +13,7 @@
using Altinn.App.Core.Internal.Auth;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Events;
+using Altinn.App.Core.Internal.InstanceLocking;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Profile;
using Altinn.App.Core.Internal.Registers;
@@ -115,6 +116,7 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con
services.AddTransient>();
services.AddTransient();
services.AddTransient();
+ services.AddScoped();
services.PostConfigureAll(options =>
{
From 312166d99f0e3701825b251b2158c8d6a561d366 Mon Sep 17 00:00:00 2001
From: Tobias Netskar <51908451+vxkc@users.noreply.github.com>
Date: Wed, 28 Jan 2026 13:51:20 +0100
Subject: [PATCH 17/17] Mock InstanceLocker in Altinn.App.Core tests
---
.../Internal/Process/ProcessEngineTest.cs | 53 ++++++++++++++-----
1 file changed, 39 insertions(+), 14 deletions(-)
diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs
index 6978256137..0f35e4f9ad 100644
--- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs
+++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs
@@ -9,6 +9,7 @@
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.AppModel;
using Altinn.App.Core.Internal.Data;
+using Altinn.App.Core.Internal.InstanceLocking;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Process;
using Altinn.App.Core.Internal.Process.Elements;
@@ -47,7 +48,7 @@ public ProcessEngineTest(ITestOutputHelper output)
[Fact]
public async Task StartProcess_returns_unsuccessful_when_process_already_started()
{
- using var fixture = Fixture.Create();
+ await using var fixture = Fixture.Create();
ProcessEngine processEngine = fixture.ProcessEngine;
Instance instance = new Instance()
{
@@ -69,7 +70,7 @@ public async Task StartProcess_returns_unsuccessful_when_no_matching_startevent_
processReaderMock.Setup(r => r.GetStartEventIds()).Returns(new List() { "StartEvent_1" });
var services = new ServiceCollection();
services.AddSingleton(processReaderMock.Object);
- using var fixture = Fixture.Create(services);
+ await using var fixture = Fixture.Create(services);
ProcessEngine processEngine = fixture.ProcessEngine;
Instance instance = new Instance() { Id = _instanceId, AppId = "org/app" };
ProcessStartRequest processStartRequest = new ProcessStartRequest()
@@ -87,7 +88,7 @@ public async Task StartProcess_returns_unsuccessful_when_no_matching_startevent_
[Fact]
public async Task StartProcess_starts_process_and_moves_to_first_task_without_event_dispatch_when_dryrun()
{
- using var fixture = Fixture.Create();
+ await using var fixture = Fixture.Create();
ProcessEngine processEngine = fixture.ProcessEngine;
Instance instance = new Instance()
{
@@ -121,7 +122,7 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_without_ev
[ClassData(typeof(TestAuthentication.AllTokens))]
public async Task StartProcess_starts_process_and_moves_to_first_task(TestJwtToken token)
{
- using var fixture = Fixture.Create(withTelemetry: true, token: token);
+ await using var fixture = Fixture.Create(withTelemetry: true, token: token);
var instanceOwnerPartyId = token.Auth switch
{
Authenticated.User auth => auth.SelectedPartyId,
@@ -255,7 +256,7 @@ Authenticated.SystemUser auth when await auth.LoadDetails() is { } details => de
[Fact]
public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefill()
{
- using var fixture = Fixture.Create();
+ await using var fixture = Fixture.Create();
ProcessEngine processEngine = fixture.ProcessEngine;
Instance instance = new Instance()
{
@@ -409,7 +410,7 @@ public async Task Next_returns_unsuccessful_for_invalid_process_states(
string expectedErrorMessage
)
{
- using var fixture = Fixture.Create();
+ await using var fixture = Fixture.Create();
ProcessEngine processEngine = fixture.ProcessEngine;
var instance = new Instance()
@@ -456,7 +457,10 @@ public async Task HandleUserAction_returns_successful_when_handler_succeeds()
.Setup(u => u.HandleAction(It.IsAny()))
.ReturnsAsync(UserActionResult.SuccessResult());
- using var fixture = Fixture.Create(updatedInstance: expectedInstance, userActions: [userActionMock.Object]);
+ await using var fixture = Fixture.Create(
+ updatedInstance: expectedInstance,
+ userActions: [userActionMock.Object]
+ );
fixture
.Mock()
.Setup(x => x.GetApplicationMetadata())
@@ -528,7 +532,10 @@ public async Task HandleUserAction_returns_unsuccessful_unauthorized_when_action
)
);
- using var fixture = Fixture.Create(updatedInstance: expectedInstance, userActions: [userActionMock.Object]);
+ await using var fixture = Fixture.Create(
+ updatedInstance: expectedInstance,
+ userActions: [userActionMock.Object]
+ );
fixture
.Mock