Skip to content

Commit 462d957

Browse files
authored
feat: resolve and upsert CCR roles from A2 command (#411)
1 parent 92b8b8f commit 462d957

File tree

9 files changed

+310
-18
lines changed

9 files changed

+310
-18
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#nullable enable
2+
3+
using System.Diagnostics.Metrics;
4+
using Altinn.Authorization.ServiceDefaults.MassTransit;
5+
using Altinn.Register.Contracts.ExternalRoles;
6+
using Altinn.Register.Core;
7+
using Altinn.Register.Core.ExternalRoles;
8+
using MassTransit;
9+
10+
namespace Altinn.Register.PartyImport.A2;
11+
12+
/// <summary>
13+
/// Consumer for resolving A2 external role assignments.
14+
/// </summary>
15+
public sealed partial class A2ExternalRoleResolverConsumer
16+
: IConsumer<ResolveAndUpsertA2CCRRoleAssignmentsCommand>
17+
{
18+
private readonly ILogger<A2ExternalRoleResolverConsumer> _logger;
19+
private readonly IExternalRoleDefinitionPersistence _persistence;
20+
private readonly ICommandSender _sender;
21+
private readonly ImportMeters _meters;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="A2ExternalRoleResolverConsumer"/> class.
25+
/// </summary>
26+
public A2ExternalRoleResolverConsumer(
27+
ILogger<A2ExternalRoleResolverConsumer> logger,
28+
IExternalRoleDefinitionPersistence externalRoleDefinitionPersistence,
29+
ICommandSender sender,
30+
RegisterTelemetry telemetry)
31+
{
32+
_logger = logger;
33+
_persistence = externalRoleDefinitionPersistence;
34+
_sender = sender;
35+
_meters = telemetry.GetServiceMeters<ImportMeters>();
36+
}
37+
38+
/// <inheritdoc/>
39+
public async Task Consume(ConsumeContext<ResolveAndUpsertA2CCRRoleAssignmentsCommand> context)
40+
{
41+
if (context.Message.RoleAssignments.Count == 0)
42+
{
43+
await _sender.Send(new UpsertExternalRoleAssignmentsCommand
44+
{
45+
FromPartyUuid = context.Message.FromPartyUuid,
46+
Source = ExternalRoleSource.CentralCoordinatingRegister,
47+
Assignments = [],
48+
Tracking = context.Message.Tracking,
49+
});
50+
51+
return;
52+
}
53+
54+
var assignments = new List<UpsertExternalRoleAssignmentsCommand.Assignment>(context.Message.RoleAssignments.Count);
55+
foreach (var assignment in context.Message.RoleAssignments)
56+
{
57+
var roleDefinition = await _persistence.TryGetRoleDefinitionByRoleCode(assignment.RoleCode, context.CancellationToken);
58+
if (roleDefinition is null)
59+
{
60+
Log.RoleWithRoleCodeNotFound(_logger, assignment.RoleCode, context.Message.FromPartyUuid, assignment.ToPartyUuid);
61+
_meters.RoleDefinitionsNotFound.Add(1);
62+
continue;
63+
}
64+
65+
if (roleDefinition.Source != ExternalRoleSource.CentralCoordinatingRegister)
66+
{
67+
Log.RoleWithWrongSource(_logger, assignment.RoleCode, context.Message.FromPartyUuid, assignment.ToPartyUuid, roleDefinition.Source, roleDefinition.Identifier);
68+
_meters.RoleDefinitionsWrongSource.Add(1);
69+
continue;
70+
}
71+
72+
_meters.RoleDefinitionsResolved.Add(1);
73+
assignments.Add(new()
74+
{
75+
Identifier = roleDefinition.Identifier,
76+
ToPartyUuid = assignment.ToPartyUuid,
77+
});
78+
}
79+
80+
await _sender.Send(
81+
new UpsertExternalRoleAssignmentsCommand
82+
{
83+
FromPartyUuid = context.Message.FromPartyUuid,
84+
Source = ExternalRoleSource.CentralCoordinatingRegister,
85+
Assignments = assignments,
86+
Tracking = context.Message.Tracking,
87+
},
88+
context.CancellationToken);
89+
}
90+
91+
private static partial class Log
92+
{
93+
[LoggerMessage(0, LogLevel.Warning, "External role-definition with role code '{RoleCode}' not found. Attempted to add from party '{FromPartyUuid}' to party '{ToPartyUuid}'.", EventName = "RoleWithRoleCodeNotFound")]
94+
public static partial void RoleWithRoleCodeNotFound(ILogger logger, string roleCode, Guid fromPartyUuid, Guid toPartyUuid);
95+
96+
[LoggerMessage(1, LogLevel.Warning, "External role-definition with role code '{RoleCode}' found, but with the wrong source '{Source}' and identifier '{Identifier}'. Attempted to add from party '{FromPartyUuid}' to party '{ToPartyUuid}'.", EventName = "RoleWithWrongSource")]
97+
public static partial void RoleWithWrongSource(ILogger logger, string roleCode, Guid fromPartyUuid, Guid toPartyUuid, ExternalRoleSource source, string identifier);
98+
}
99+
100+
/// <summary>
101+
/// Consumer definition for <see cref="A2ExternalRoleResolverConsumer"/>.
102+
/// </summary>
103+
public sealed class Definition
104+
: ConsumerDefinition<A2ExternalRoleResolverConsumer>
105+
{
106+
/// <inheritdoc/>
107+
protected override void ConfigureConsumer(
108+
IReceiveEndpointConfigurator endpointConfigurator,
109+
IConsumerConfigurator<A2ExternalRoleResolverConsumer> consumerConfigurator,
110+
IRegistrationContext context)
111+
{
112+
base.ConfigureConsumer(endpointConfigurator, consumerConfigurator, context);
113+
}
114+
}
115+
116+
/// <summary>
117+
/// Meters for <see cref="PartyImportBatchConsumer"/>.
118+
/// </summary>
119+
private sealed class ImportMeters(RegisterTelemetry telemetry)
120+
: IServiceMeters<ImportMeters>
121+
{
122+
/// <summary>
123+
/// Gets a counter for the number of parties upserted.
124+
/// </summary>
125+
public Counter<int> RoleDefinitionsNotFound { get; }
126+
= telemetry.CreateCounter<int>("register.party-import.a2.resolve-role.errors", description: "The number of times a role-code was not found.");
127+
128+
public Counter<int> RoleDefinitionsResolved { get; }
129+
= telemetry.CreateCounter<int>("register.party-import.a2.resolve-role.found", description: "The number of times a role-code was resolved.");
130+
131+
public Counter<int> RoleDefinitionsWrongSource { get; }
132+
= telemetry.CreateCounter<int>("register.party-import.a2.resolve-role.wrong-source", description: "The number of times a role-code was found, but with the wrong source.");
133+
134+
/// <inheritdoc/>
135+
public static ImportMeters Create(RegisterTelemetry telemetry)
136+
=> new ImportMeters(telemetry);
137+
}
138+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#nullable enable
2+
3+
using Altinn.Authorization.ServiceDefaults.MassTransit;
4+
using Altinn.Register.Core.PartyImport.A2;
5+
6+
namespace Altinn.Register.PartyImport.A2;
7+
8+
/// <summary>
9+
/// A command for resolving external role assignments fetched from A2
10+
/// and upserting them.
11+
/// </summary>
12+
public sealed record ResolveAndUpsertA2CCRRoleAssignmentsCommand
13+
: CommandBase
14+
{
15+
/// <summary>
16+
/// Gets the party UUID that the role assignments are from.
17+
/// </summary>
18+
public required Guid FromPartyUuid { get; init; }
19+
20+
/// <summary>
21+
/// Gets the role assignments.
22+
/// </summary>
23+
public required IReadOnlyList<A2PartyExternalRoleAssignment> RoleAssignments { get; init; }
24+
25+
/// <summary>
26+
/// Gets the tracking information for the import.
27+
/// </summary>
28+
public required UpsertPartyTracking Tracking { get; init; }
29+
}

src/Altinn.Register/src/Altinn.Register/PartyImport/PartyImportBatchConsumer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ protected override void ConfigureConsumer(
300300
{
301301
options
302302
.SetMessageLimit(BATCH_SIZE)
303-
.SetTimeLimit(_isTest ? TimeSpan.FromSeconds(0.1) : TimeSpan.FromSeconds(15))
303+
.SetTimeLimit(_isTest ? TimeSpan.FromSeconds(1) : TimeSpan.FromSeconds(15))
304304
.SetTimeLimitStart(BatchTimeLimitStart.FromFirst)
305305
.SetConcurrencyLimit(1);
306306
});

src/Altinn.Register/test/Altinn.Register.TestUtils/BusTestBase.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public abstract class BusTestBase(ITestOutputHelper output)
4848
protected override ValueTask ConfigureServices(IServiceCollection services)
4949
{
5050
services.AddHttpClient<IA2PartyImportService, A2PartyImportService>();
51+
services.AddTelemetryListener(new StringWriter(_harnessLogger), includeDetails: true);
5152
AltinnServiceDefaultsMassTransitTestingExtensions.AddAltinnMassTransitTestHarness(
5253
services,
5354
output: new StringWriter(_harnessLogger),
@@ -78,15 +79,19 @@ protected override async ValueTask DisposeAsync()
7879
{
7980
_harness.ForceInactive();
8081
_harness.Cancel();
81-
await _harness.OutputTimeline(new StringWriter(_harnessLogger));
82-
output.WriteLine(_harnessLogger.ToString());
8382

84-
await foreach (var consumeException in _harness.Consumed.SelectAsync(static m => m.Exception is not null).Select(static m => m.Exception))
83+
var allExceptions = _harness.Consumed.SelectAsync(static m => m.Exception is not null).Select(static m => m.Exception)
84+
.Concat(_harness.Sent.SelectAsync(static m => m.Exception is not null).Select(static m => m.Exception))
85+
.Concat(_harness.Published.SelectAsync(static m => m.Exception is not null).Select(static m => m.Exception));
86+
87+
await foreach (var consumeException in allExceptions)
8588
{
8689
output.WriteLine(consumeException.ToString());
8790
}
8891
}
8992

9093
await base.DisposeAsync();
94+
95+
output.WriteLine(_harnessLogger.ToString());
9196
}
9297
}

src/Altinn.Register/test/Altinn.Register.TestUtils/MassTransit/TestHarnessConversation.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,10 @@ internal TestHarnessConversation(ITestHarness harness, Guid conversationId)
2727
/// </summary>
2828
public AsyncUnwrappedEnumerable<EventBase, IPublishedMessage<EventBase>> Events
2929
=> new(_harness.Published.SelectAsync<EventBase>(m => m.Context.ConversationId == _conversationId), static m => m.Context.Message);
30+
31+
/// <summary>
32+
/// Gets the commands in the conversation.
33+
/// </summary>
34+
public AsyncUnwrappedEnumerable<CommandBase, ISentMessage<CommandBase>> Commands
35+
=> new(_harness.Sent.SelectAsync<CommandBase>(m => m.Context.ConversationId == _conversationId), static m => m.Context.Message);
3036
}

src/Altinn.Register/test/Altinn.Register.TestUtils/MassTransit/TestHarnessExtensions.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using MassTransit.Testing;
1+
using Altinn.Authorization.ServiceDefaults.MassTransit;
2+
using MassTransit.Testing;
23

34
namespace Altinn.Register.TestUtils.MassTransit;
45

@@ -15,4 +16,21 @@ public static class TestHarnessExtensions
1516
/// <returns>A <see cref="TestHarnessConversation"/>.</returns>
1617
public static TestHarnessConversation Conversation(this ITestHarness harness, Guid guid)
1718
=> new(harness, guid);
19+
20+
/// <summary>
21+
/// Gets a conversation helper by the initial command.
22+
/// </summary>
23+
/// <typeparam name="TCommand">The command type.</typeparam>
24+
/// <param name="harness">The <see cref="ITestHarness"/>.</param>
25+
/// <param name="command">The initial command that started the conversation.</param>
26+
/// <returns>A <see cref="TestHarnessConversation"/>.</returns>
27+
public static async Task<TestHarnessConversation> Conversation<TCommand>(
28+
this ITestHarness harness,
29+
TCommand command)
30+
where TCommand : CommandBase
31+
{
32+
var consumed = await harness.Consumed.SelectAsync<TCommand>(m => m.Context.CorrelationId == command.CommandId).FirstAsync();
33+
34+
return harness.Conversation(consumed.Context.ConversationId!.Value);
35+
}
1836
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#nullable enable
2+
3+
using Altinn.Register.Contracts.ExternalRoles;
4+
using Altinn.Register.Core.ImportJobs;
5+
using Altinn.Register.Core.UnitOfWork;
6+
using Altinn.Register.PartyImport;
7+
using Altinn.Register.PartyImport.A2;
8+
using Altinn.Register.TestUtils;
9+
using Altinn.Register.TestUtils.MassTransit;
10+
using Altinn.Register.TestUtils.TestData;
11+
using Xunit.Abstractions;
12+
13+
namespace Altinn.Register.Tests.PartyImport.A2;
14+
15+
public class A2ExternalRoleResolverConsumerTests(ITestOutputHelper output)
16+
: BusTestBase(output)
17+
{
18+
protected override bool SeedData => false;
19+
20+
////protected override ITestOutputHelper? TestOutputHelper => output;
21+
22+
[Fact]
23+
public async Task ResolveAndUpsertA2CCRRoleAssignmentsCommand_LooksUpRoles_AndUpserts()
24+
{
25+
await GetRequiredService<IImportJobTracker>().TrackQueueStatus("test", new()
26+
{
27+
SourceMax = 10,
28+
EnqueuedMax = 1,
29+
});
30+
var (org, person1, person2) = await Setup(async uow =>
31+
{
32+
var org = await uow.CreateOrg();
33+
var person1 = await uow.CreatePerson();
34+
var person2 = await uow.CreatePerson();
35+
36+
var roles = uow.GetPartyExternalRolePersistence();
37+
await roles.UpsertExternalRolesFromPartyBySource(
38+
commandId: Guid.CreateVersion7(),
39+
partyUuid: org.PartyUuid.Value,
40+
roleSource: ExternalRoleSource.CentralCoordinatingRegister,
41+
assignments: [
42+
new("lede", person2.PartyUuid.Value),
43+
]);
44+
45+
return (org, person1, person2);
46+
});
47+
48+
var cmd = new ResolveAndUpsertA2CCRRoleAssignmentsCommand
49+
{
50+
FromPartyUuid = org.PartyUuid.Value,
51+
Tracking = new("test", 1),
52+
RoleAssignments = [
53+
new() { ToPartyUuid = person1.PartyUuid.Value, RoleCode = "DAGL" },
54+
new() { ToPartyUuid = person1.PartyUuid.Value, RoleCode = "MEDL" },
55+
new() { ToPartyUuid = person2.PartyUuid.Value, RoleCode = "MEDL" },
56+
new() { ToPartyUuid = person2.PartyUuid.Value, RoleCode = "NON_EXISTING" },
57+
],
58+
};
59+
60+
await CommandSender.Send(cmd);
61+
var conversation = await Harness.Conversation(cmd);
62+
63+
var upsertCommand = await conversation.Commands.OfType<UpsertExternalRoleAssignmentsCommand>().FirstOrDefaultAsync();
64+
Assert.NotNull(upsertCommand);
65+
66+
Assert.Equal(ExternalRoleSource.CentralCoordinatingRegister, upsertCommand.Source);
67+
Assert.Equal(org.PartyUuid.Value, upsertCommand.FromPartyUuid);
68+
Assert.Equal(cmd.Tracking, upsertCommand.Tracking);
69+
Assert.Collection(
70+
upsertCommand.Assignments,
71+
a =>
72+
{
73+
Assert.Equal(person1.PartyUuid.Value, a.ToPartyUuid);
74+
Assert.Equal("dagl", a.Identifier);
75+
},
76+
a =>
77+
{
78+
Assert.Equal(person1.PartyUuid.Value, a.ToPartyUuid);
79+
Assert.Equal("medl", a.Identifier);
80+
},
81+
a =>
82+
{
83+
Assert.Equal(person2.PartyUuid.Value, a.ToPartyUuid);
84+
Assert.Equal("medl", a.Identifier);
85+
});
86+
}
87+
88+
private async Task<T> Setup<T>(Func<IUnitOfWork, Task<T>> setup)
89+
{
90+
await using var uow = await GetRequiredService<IUnitOfWorkManager>().CreateAsync(activityName: $"{nameof(PartyImportBatchConsumerTests)}.{nameof(Setup)}");
91+
var result = await setup(uow);
92+
await uow.CommitAsync();
93+
94+
return result;
95+
}
96+
}

src/Altinn.Register/test/Altinn.Register.Tests/PartyImport/A2PartyImportConsumerTests.cs renamed to src/Altinn.Register/test/Altinn.Register.Tests/PartyImport/A2/A2PartyImportConsumerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
using V1Models = Altinn.Platform.Register.Models;
1818

19-
namespace Altinn.Register.Tests.PartyImport;
19+
namespace Altinn.Register.Tests.PartyImport.A2;
2020

2121
public class A2PartyImportConsumerTests(ITestOutputHelper output)
2222
: BusTestBase(output)

0 commit comments

Comments
 (0)