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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Altinn.Correspondence.Persistence;
using Microsoft.EntityFrameworkCore;
using Npgsql;

namespace Altinn.Correspondence.Tests.Fixtures;

/// <summary>
/// Simulates a concurrent idempotency insert losing the race on the batched flush inside
/// <see cref="Altinn.Correspondence.Application.Helpers.DatabaseTransactionHelper.ExecuteAsync"/>.
/// </summary>
public sealed class UniqueViolationOnDeferredSaveDbContext(
DbContextOptions<ApplicationDbContext> options,
int uniqueViolationOnDeferredSaveAttempt = 1)
: TestApplicationDbContext(options)
{
private int _deferredSaveAttempts;

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
if (DeferSaveChanges)
{
_deferredSaveAttempts++;
if (_deferredSaveAttempts == uniqueViolationOnDeferredSaveAttempt)
{
throw new DbUpdateException(
"duplicate key",
new PostgresException(
"duplicate key value violates unique constraint",
"ERROR",
"ERROR",
"23505"));
}
}

return await base.SaveChangesAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Altinn.Correspondence.Persistence;
using Altinn.Correspondence.Persistence.Helpers;
using Altinn.Correspondence.Tests.Fixtures;
using Altinn.Correspondence.Tests.Fixtures;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
Expand All @@ -17,6 +18,9 @@ public static class TestDbContextFactory

public static TestApplicationDbContext Create() => new(Options.Value);

public static UniqueViolationOnDeferredSaveDbContext CreateUniqueViolationOnDeferredSave(int onAttempt = 1)
=> new(Options.Value, onAttempt);

public static TestApplicationDbContext Create(int maxRetryCount)
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
Expand Down Expand Up @@ -48,7 +52,7 @@ private static void ConfigureExecutionStrategy(NpgsqlDbContextOptionsBuilder npg
}
else
{
npgsql.ExecutionStrategy(dependencies =>
npgsql.ExecutionStrategy(dependencies =>
new CorrespondenceNpgsqlRetryingExecutionStrategy(dependencies));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Security.Claims;
using Altinn.Correspondence.API.Auth;
using Altinn.Correspondence.Common.Constants;
using Microsoft.AspNetCore.Authorization;

namespace Altinn.Correspondence.Tests.TestingAPI;

public class AuthorizationPolicyTests
{
[Theory]
[InlineData("https://platform.yt01.altinn.cloud")]
[InlineData("https://platform.tt02.altinn.no")]
[InlineData("https://platform.tt02.altinn.no/")]
public void RecipientScopePolicy_AcceptsConfiguredAltinnIssuer(string platformGatewayUrl)
{
var context = CreateContext(
$"{platformGatewayUrl.TrimEnd('/')}/authentication/api/v1/openid/",
AuthorizationConstants.RecipientScope);

var authorized = DependencyInjection.RecipientScopePolicy(context, platformGatewayUrl);

Assert.True(authorized);
}

[Fact]
public void RecipientScopePolicy_RejectsDifferentAltinnIssuer()
{
var context = CreateContext(
"https://platform.other.altinn.cloud/authentication/api/v1/openid/",
AuthorizationConstants.RecipientScope);

var authorized = DependencyInjection.RecipientScopePolicy(
context,
"https://platform.yt01.altinn.cloud");

Assert.False(authorized);
}

[Fact]
public void RecipientScopePolicy_RejectsMissingRecipientScope()
{
var platformGatewayUrl = "https://platform.yt01.altinn.cloud";
var context = CreateContext(
$"{platformGatewayUrl}/authentication/api/v1/openid/",
AuthorizationConstants.SenderScope);

var authorized = DependencyInjection.RecipientScopePolicy(context, platformGatewayUrl);

Assert.False(authorized);
}

private static AuthorizationHandlerContext CreateContext(string issuer, string scope)
{
var principal = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim("iss", issuer),
new Claim("scope", scope),
], "Test"));

return new AuthorizationHandlerContext([], principal, resource: null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Altinn.Correspondence.Application.Helpers;
using Altinn.Correspondence.Application.InitializeCorrespondences;
using Altinn.Correspondence.Common.Constants;
using Altinn.Correspondence.Core.Models.Entities;
using Altinn.Correspondence.Core.Repositories;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;

namespace Altinn.Correspondence.Tests.TestingApplication;

public class InitializeCorrespondenceHelperTests
{
[Fact]
public async Task MapToCorrespondenceEntity_ReferencesInitializedAttachmentById()
{
var serviceOwnerHelper = new ServiceOwnerHelper(
Mock.Of<IServiceOwnerRepository>(),
NullLogger<ServiceOwnerHelper>.Instance);
var helper = new InitializeCorrespondenceHelper(
Mock.Of<IAttachmentRepository>(),
null!,
null!,
serviceOwnerHelper,
NullLogger<InitializeCorrespondenceHelper>.Instance);
var attachmentId = Guid.NewGuid();
var attachment = new AttachmentEntity
{
Id = attachmentId,
ResourceId = "test-resource",
FileName = "attachment.txt",
SendersReference = "attachment-reference",
Sender = $"{UrnConstants.OrganizationNumberAttribute}:991825827",
Created = DateTimeOffset.UtcNow,
};
var request = new InitializeCorrespondencesRequest
{
Correspondence = new CorrespondenceEntity
{
ResourceId = "test-resource",
Recipient = string.Empty,
Sender = $"{UrnConstants.OrganizationNumberAttribute}:991825827",
SendersReference = "correspondence-reference",
Content = new CorrespondenceContentEntity
{
Language = "nb",
MessageTitle = "Title",
MessageSummary = "Summary",
MessageBody = "Body",
Attachments = [],
},
RequestedPublishTime = DateTimeOffset.UtcNow,
Created = DateTimeOffset.UtcNow,
Statuses = [],
},
Recipients = ["urn:altinn:person:identifier-no:14886498226"],
};

var result = await helper.MapToCorrespondenceEntityAsync(
request,
request.Recipients.Single(),
[attachment],
Guid.NewGuid(),
null,
false,
"991825827",
CancellationToken.None);

var correspondenceAttachment = Assert.Single(result.Content.Attachments);
Assert.Equal(attachmentId, correspondenceAttachment.AttachmentId);
Assert.Null(correspondenceAttachment.Attachment);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public async Task Delete_Correspondence_Also_deletes_attachment()

// Assert
var attachment = await _senderClient.GetFromJsonAsync<AttachmentOverviewExt>($"correspondence/api/v1/attachment/{correspondenceResponse.AttachmentIds.FirstOrDefault()}", _responseSerializerOptions);
Assert.Equal(attachment?.Status, AttachmentStatusExt.Purged);
Assert.Equal(AttachmentStatusExt.Purged, attachment?.Status);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,32 @@ public async Task InitializeCorrespondence_WithDuplicateIdempotentKey_ReturnsCon
Assert.Contains(CorrespondenceErrors.DuplicateInitCorrespondenceRequest.Message, errorContent);
}

[Fact]
public async Task InitializeCorrespondence_WithDuplicateIdempotentKey_SkipsValidation()
{
var idempotentKey = Guid.NewGuid();
var validCorrespondence = new CorrespondenceBuilder()
.CreateCorrespondence()
.WithIdempotentKey(idempotentKey)
.Build();

var firstResponse = await _senderClient.PostAsJsonAsync("correspondence/api/v1/correspondence", validCorrespondence);
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);

var invalidIfValidated = new CorrespondenceBuilder()
.CreateCorrespondence()
.WithIdempotentKey(idempotentKey)
.WithExternalReferencesDialogId("not-a-guid")
.Build();

var duplicateResponse = await _senderClient.PostAsJsonAsync("correspondence/api/v1/correspondence", invalidIfValidated);

Assert.Equal(HttpStatusCode.Conflict, duplicateResponse.StatusCode);
var errorContent = await duplicateResponse.Content.ReadAsStringAsync();
Assert.Contains(CorrespondenceErrors.DuplicateInitCorrespondenceRequest.Message, errorContent);
Assert.DoesNotContain(CorrespondenceErrors.InvalidCorrespondenceDialogId.Message, errorContent);
}

[Fact]
public async Task InitializeCorrespondence_WithDifferentContentAndSameIdempotentKey_ShouldReturnConflict()
{
Expand Down Expand Up @@ -2570,10 +2596,20 @@ public async Task WhenDialogCreationFails_PublishCorrespondenceHandlerIsStillSch
Statuses = new List<CorrespondenceStatusEntity>()
});

var idempotencyKeyRepositoryMock = new Mock<IIdempotencyKeyRepository>();
idempotencyKeyRepositoryMock
.Setup(x => x.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IdempotencyKeyEntity?)null);
idempotencyKeyRepositoryMock
.Setup(x => x.CreateAsync(It.IsAny<IdempotencyKeyEntity>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IdempotencyKeyEntity entity, CancellationToken _) => entity);

var helper = new HangfireScheduleHelper(
scheduleClientMock.Object,
hybridCacheWrapperMock.Object,
scheduleRepositoryMock.Object,
idempotencyKeyRepositoryMock.Object,
TestDbContextFactory.Create(),
new Mock<ILogger<HangfireScheduleHelper>>().Object);

await helper.SchedulePublishAfterDialogCreated(correspondenceId, CancellationToken.None);
Expand Down Expand Up @@ -2601,12 +2637,15 @@ s is AwaitingState &&
var correspondenceRepositoryMock = new Mock<ICorrespondenceRepository>();
var correspondenceStatusRepositoryMock = new Mock<ICorrespondenceStatusRepository>();
var altinnRegisterServiceMock = new Mock<IAltinnRegisterService>();
var idempotencyKeyRepositoryMock = new Mock<IIdempotencyKeyRepository>();
var publishIdempotencyKeyRepositoryMock = new Mock<IIdempotencyKeyRepository>();

publishClientMock
.Setup(x => x.Create(It.IsAny<Job>(), It.IsAny<IState>()))
.Returns(() => Guid.NewGuid().ToString());
idempotencyKeyRepositoryMock
publishIdempotencyKeyRepositoryMock
.Setup(x => x.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IdempotencyKeyEntity?)null);
publishIdempotencyKeyRepositoryMock
.Setup(x => x.CreateAsync(It.IsAny<IdempotencyKeyEntity>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IdempotencyKeyEntity key, CancellationToken _) => key);

Expand Down Expand Up @@ -2660,7 +2699,7 @@ s is AwaitingState &&
correspondenceStatusRepositoryMock.Object,
new Mock<IContactReservationRegistryService>().Object,
publishClientMock.Object,
idempotencyKeyRepositoryMock.Object,
publishIdempotencyKeyRepositoryMock.Object,
TestDbContextFactory.Create());

await publishHandler.Process(correspondenceId, null, CancellationToken.None);
Expand Down
Loading
Loading