From e2bedb3beb592b809348280801e1fe43cf363692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 04:10:17 -0600 Subject: [PATCH] feat(thalos-bff): integrate identity edge contract adapters --- docs/api/identity-edge-api.md | 3 + docs/architecture/thalos-bff-flow.puml | 8 ++- docs/security/permission-enforcement-map.md | 4 +- .../Adapters/IIdentityEdgeContractAdapter.cs | 47 +++++++++++++ .../IIdentityEdgeGrpcContractAdapter.cs | 24 +++++++ .../Adapters/IThalosServiceClient.cs | 16 +++-- .../RefreshIdentitySessionRequest.cs | 8 +++ .../RefreshIdentitySessionResponse.cs | 8 +++ .../Grpc/IssueIdentityTokenGrpcContract.cs | 9 +++ .../Handlers/IssueTokenHandler.cs | 16 +++-- .../Handlers/RefreshSessionHandler.cs | 10 ++- .../Security/IPermissionGuard.cs | 6 +- .../Thalos.Bff.Application.csproj | 1 + .../Api/IssueTokenApiRequest.cs | 3 +- .../Api/RefreshSessionApiRequest.cs | 3 +- .../Conventions/ThalosBffPackageContract.cs | 15 +++++ .../Thalos.Bff.Contracts.csproj | 3 + .../ContractShapeTests.cs | 29 ++++++++ .../IssueTokenHandlerTests.cs | 52 ++++++++++++-- .../RefreshSessionHandlerTests.cs | 67 +++++++++++++++++++ 20 files changed, 307 insertions(+), 25 deletions(-) create mode 100644 src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs create mode 100644 src/Thalos.Bff.Application/Adapters/IIdentityEdgeGrpcContractAdapter.cs create mode 100644 src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs create mode 100644 src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs create mode 100644 src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs create mode 100644 src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs create mode 100644 tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs create mode 100644 tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs diff --git a/docs/api/identity-edge-api.md b/docs/api/identity-edge-api.md index c61beab..0423fa2 100644 --- a/docs/api/identity-edge-api.md +++ b/docs/api/identity-edge-api.md @@ -3,6 +3,7 @@ ## Active External Protocol - REST is the active external protocol for this BFF deployment. +- Internal service calls default to gRPC-adapted contracts. ## Entrypoints @@ -12,5 +13,7 @@ ## Boundary Notes - Endpoint handlers perform edge validation and permission checks. +- Token issuance and policy evaluation requests are mapped to thalos-service identity contracts. +- Session refresh requests are mapped through edge contract adapters before downstream calls. - Business orchestration remains in thalos-service. - Identity abstractions remain owned by Thalos repositories. diff --git a/docs/architecture/thalos-bff-flow.puml b/docs/architecture/thalos-bff-flow.puml index ec2a6b0..4734622 100644 --- a/docs/architecture/thalos-bff-flow.puml +++ b/docs/architecture/thalos-bff-flow.puml @@ -8,11 +8,17 @@ package "thalos-bff" { interface IRefreshSessionHandler class RefreshSessionHandler interface IPermissionGuard + interface IIdentityEdgeContractAdapter + interface IIdentityEdgeGrpcContractAdapter interface IThalosServiceClient + class IssueIdentityTokenGrpcContract IssueTokenHandler ..|> IIssueTokenHandler RefreshSessionHandler ..|> IRefreshSessionHandler IssueTokenHandler --> IPermissionGuard + IssueTokenHandler --> IIdentityEdgeContractAdapter + RefreshSessionHandler --> IIdentityEdgeContractAdapter + IIdentityEdgeGrpcContractAdapter --> IssueIdentityTokenGrpcContract IssueTokenHandler --> IThalosServiceClient RefreshSessionHandler --> IThalosServiceClient } @@ -23,5 +29,5 @@ package "thalos-service" as ThalosService Clients --> Program : REST Program --> IIssueTokenHandler Program --> IRefreshSessionHandler -IThalosServiceClient ..> ThalosService : gRPC/internal +IThalosServiceClient ..> ThalosService : gRPC/internal contracts @enduml diff --git a/docs/security/permission-enforcement-map.md b/docs/security/permission-enforcement-map.md index e6592db..d72d296 100644 --- a/docs/security/permission-enforcement-map.md +++ b/docs/security/permission-enforcement-map.md @@ -2,10 +2,10 @@ ## Enforcement Points -- `identity.token.issue` evaluated at token issuance handler. +- `identity.token.issue` evaluated via thalos-service policy contract before token issuance. - Session refresh guarded by edge session validation policy. ## Guardrail -- Permission checks happen at BFF entrypoints before downstream calls. +- Permission checks happen at BFF entrypoints using thalos-service policy responses. - Authorization decisions are explicit and traceable at edge boundaries. diff --git a/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs new file mode 100644 index 0000000..924a7f9 --- /dev/null +++ b/src/Thalos.Bff.Application/Adapters/IIdentityEdgeContractAdapter.cs @@ -0,0 +1,47 @@ +using Thalos.Bff.Application.Contracts; +using Thalos.Bff.Contracts.Api; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Bff.Application.Adapters; + +/// +/// Defines adapter boundary between external edge contracts and thalos-service contracts. +/// +public interface IIdentityEdgeContractAdapter +{ + /// + /// Maps API request into identity policy evaluation request. + /// + /// External API request. + /// Permission code to evaluate. + /// Identity policy request. + EvaluateIdentityPolicyRequest ToPolicyRequest(IssueTokenApiRequest request, string permissionCode); + + /// + /// Maps API request into identity token issuance request. + /// + /// External API request. + /// Identity token issuance request. + IssueIdentityTokenRequest ToIssueTokenRequest(IssueTokenApiRequest request); + + /// + /// Maps identity token issuance response into edge API response. + /// + /// Identity token issuance response. + /// External API response. + IssueTokenApiResponse ToIssueTokenApiResponse(IssueIdentityTokenResponse response); + + /// + /// Maps refresh API request into internal refresh contract. + /// + /// External refresh API request. + /// Internal refresh request. + RefreshIdentitySessionRequest ToRefreshSessionRequest(RefreshSessionApiRequest request); + + /// + /// Maps internal refresh response into external API response. + /// + /// Internal refresh response. + /// External API response. + RefreshSessionApiResponse ToRefreshSessionApiResponse(RefreshIdentitySessionResponse response); +} diff --git a/src/Thalos.Bff.Application/Adapters/IIdentityEdgeGrpcContractAdapter.cs b/src/Thalos.Bff.Application/Adapters/IIdentityEdgeGrpcContractAdapter.cs new file mode 100644 index 0000000..477a2c4 --- /dev/null +++ b/src/Thalos.Bff.Application/Adapters/IIdentityEdgeGrpcContractAdapter.cs @@ -0,0 +1,24 @@ +using Thalos.Bff.Application.Grpc; +using Thalos.Bff.Contracts.Api; + +namespace Thalos.Bff.Application.Adapters; + +/// +/// Defines adapter boundary for gRPC contract translation at the identity edge. +/// +public interface IIdentityEdgeGrpcContractAdapter +{ + /// + /// Maps external API request into gRPC contract shape. + /// + /// External token issuance request. + /// gRPC token issuance contract. + IssueIdentityTokenGrpcContract ToGrpc(IssueTokenApiRequest request); + + /// + /// Maps gRPC contract shape into external API request. + /// + /// gRPC token issuance contract. + /// External token issuance request. + IssueTokenApiRequest FromGrpc(IssueIdentityTokenGrpcContract contract); +} diff --git a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs index 667d149..83e30d5 100644 --- a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs +++ b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs @@ -1,4 +1,5 @@ -using Thalos.Bff.Contracts.Api; +using Thalos.Bff.Application.Contracts; +using Thalos.Service.Identity.Abstractions.Contracts; namespace Thalos.Bff.Application.Adapters; @@ -10,14 +11,21 @@ public interface IThalosServiceClient /// /// Requests token issuance from thalos-service. /// - /// Token issuance request. + /// Identity token issuance request. /// Token issuance response. - Task IssueTokenAsync(IssueTokenApiRequest request); + Task IssueTokenAsync(IssueIdentityTokenRequest request); + + /// + /// Requests policy evaluation from thalos-service. + /// + /// Identity policy request. + /// Identity policy response. + Task EvaluatePolicyAsync(EvaluateIdentityPolicyRequest request); /// /// Requests token refresh from thalos-service. /// /// Session refresh request. /// Session refresh response. - Task RefreshSessionAsync(RefreshSessionApiRequest request); + Task RefreshSessionAsync(RefreshIdentitySessionRequest request); } diff --git a/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs b/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs new file mode 100644 index 0000000..f4bd093 --- /dev/null +++ b/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionRequest.cs @@ -0,0 +1,8 @@ +namespace Thalos.Bff.Application.Contracts; + +/// +/// Transport-neutral internal request contract for refresh session flow. +/// +/// Refresh token value. +/// Request correlation identifier. +public sealed record RefreshIdentitySessionRequest(string RefreshToken, string CorrelationId); diff --git a/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs b/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs new file mode 100644 index 0000000..5a5267c --- /dev/null +++ b/src/Thalos.Bff.Application/Contracts/RefreshIdentitySessionResponse.cs @@ -0,0 +1,8 @@ +namespace Thalos.Bff.Application.Contracts; + +/// +/// Transport-neutral internal response contract for refresh session flow. +/// +/// Refreshed token value. +/// Token expiration in seconds. +public sealed record RefreshIdentitySessionResponse(string Token, int ExpiresInSeconds); diff --git a/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs b/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs new file mode 100644 index 0000000..ac57ab0 --- /dev/null +++ b/src/Thalos.Bff.Application/Grpc/IssueIdentityTokenGrpcContract.cs @@ -0,0 +1,9 @@ +namespace Thalos.Bff.Application.Grpc; + +/// +/// Defines minimal gRPC contract shape for identity token edge translation. +/// +/// Identity subject identifier. +/// Tenant identifier. +/// Request correlation identifier. +public sealed record IssueIdentityTokenGrpcContract(string SubjectId, string TenantId, string CorrelationId); diff --git a/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs b/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs index 2e3ec62..08da29d 100644 --- a/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs +++ b/src/Thalos.Bff.Application/Handlers/IssueTokenHandler.cs @@ -7,17 +7,25 @@ namespace Thalos.Bff.Application.Handlers; /// /// Default edge handler for token issuance. /// -public sealed class IssueTokenHandler(IThalosServiceClient serviceClient, IPermissionGuard permissionGuard) +public sealed class IssueTokenHandler( + IThalosServiceClient serviceClient, + IIdentityEdgeContractAdapter contractAdapter, + IPermissionGuard permissionGuard) : IIssueTokenHandler { /// - public Task HandleAsync(IssueTokenApiRequest request) + public async Task HandleAsync(IssueTokenApiRequest request) { - if (!permissionGuard.CanAccess("identity.token.issue")) + var policyRequest = contractAdapter.ToPolicyRequest(request, "identity.token.issue"); + var policyResponse = await serviceClient.EvaluatePolicyAsync(policyRequest); + + if (!permissionGuard.CanAccess(policyResponse)) { throw new UnauthorizedAccessException("Permission denied."); } - return serviceClient.IssueTokenAsync(request); + var issueRequest = contractAdapter.ToIssueTokenRequest(request); + var issueResponse = await serviceClient.IssueTokenAsync(issueRequest); + return contractAdapter.ToIssueTokenApiResponse(issueResponse); } } diff --git a/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs b/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs index 0ffda49..65733d0 100644 --- a/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs +++ b/src/Thalos.Bff.Application/Handlers/RefreshSessionHandler.cs @@ -6,12 +6,16 @@ namespace Thalos.Bff.Application.Handlers; /// /// Default edge handler for refresh session flow. /// -public sealed class RefreshSessionHandler(IThalosServiceClient serviceClient) +public sealed class RefreshSessionHandler( + IThalosServiceClient serviceClient, + IIdentityEdgeContractAdapter contractAdapter) : IRefreshSessionHandler { /// - public Task HandleAsync(RefreshSessionApiRequest request) + public async Task HandleAsync(RefreshSessionApiRequest request) { - return serviceClient.RefreshSessionAsync(request); + var refreshRequest = contractAdapter.ToRefreshSessionRequest(request); + var refreshResponse = await serviceClient.RefreshSessionAsync(refreshRequest); + return contractAdapter.ToRefreshSessionApiResponse(refreshResponse); } } diff --git a/src/Thalos.Bff.Application/Security/IPermissionGuard.cs b/src/Thalos.Bff.Application/Security/IPermissionGuard.cs index ac2600d..15ce8e9 100644 --- a/src/Thalos.Bff.Application/Security/IPermissionGuard.cs +++ b/src/Thalos.Bff.Application/Security/IPermissionGuard.cs @@ -1,3 +1,5 @@ +using Thalos.Service.Identity.Abstractions.Contracts; + namespace Thalos.Bff.Application.Security; /// @@ -8,7 +10,7 @@ public interface IPermissionGuard /// /// Evaluates whether a permission is satisfied for the current request context. /// - /// Permission code to evaluate. + /// Policy evaluation response. /// True when access is allowed. - bool CanAccess(string permissionCode); + bool CanAccess(EvaluateIdentityPolicyResponse policyResponse); } diff --git a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj index 99d234a..8edcb4b 100644 --- a/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj +++ b/src/Thalos.Bff.Application/Thalos.Bff.Application.csproj @@ -6,5 +6,6 @@ + diff --git a/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs b/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs index a5b423d..7014cc6 100644 --- a/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs +++ b/src/Thalos.Bff.Contracts/Api/IssueTokenApiRequest.cs @@ -5,4 +5,5 @@ namespace Thalos.Bff.Contracts.Api; /// /// Identity subject identifier. /// Tenant identifier. -public sealed record IssueTokenApiRequest(string SubjectId, string TenantId); +/// Request correlation identifier. +public sealed record IssueTokenApiRequest(string SubjectId, string TenantId, string CorrelationId = ""); diff --git a/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs index c9c8e76..a1765d3 100644 --- a/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs +++ b/src/Thalos.Bff.Contracts/Api/RefreshSessionApiRequest.cs @@ -4,4 +4,5 @@ namespace Thalos.Bff.Contracts.Api; /// External API request for refresh token session flow. /// /// Refresh token value. -public sealed record RefreshSessionApiRequest(string RefreshToken); +/// Request correlation identifier. +public sealed record RefreshSessionApiRequest(string RefreshToken, string CorrelationId = ""); diff --git a/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs b/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs new file mode 100644 index 0000000..0cc1e4c --- /dev/null +++ b/src/Thalos.Bff.Contracts/Conventions/ThalosBffPackageContract.cs @@ -0,0 +1,15 @@ +using Core.Blueprint.Common.Contracts; + +namespace Thalos.Bff.Contracts.Conventions; + +/// +/// Defines package descriptor metadata for thalos bff edge contracts. +/// +public sealed class ThalosBffPackageContract : IBlueprintPackageContract +{ + /// + public BlueprintPackageDescriptor Descriptor { get; } = new( + "Thalos.Bff.Contracts", + PackageVersionPolicy.Minor, + ["Core.Blueprint.Common", "Thalos.Service.Identity.Abstractions"]); +} diff --git a/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj b/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj index 6c3a887..04a4bbc 100644 --- a/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj +++ b/src/Thalos.Bff.Contracts/Thalos.Bff.Contracts.csproj @@ -4,4 +4,7 @@ enable enable + + + diff --git a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs new file mode 100644 index 0000000..1df0d38 --- /dev/null +++ b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs @@ -0,0 +1,29 @@ +using Core.Blueprint.Common.Contracts; +using Thalos.Bff.Contracts.Api; +using Thalos.Bff.Contracts.Conventions; + +namespace Thalos.Bff.Application.UnitTests; + +public class ContractShapeTests +{ + [Fact] + public void IssueTokenApiRequest_WhenCreated_StoresCorrelationId() + { + var request = new IssueTokenApiRequest("user-1", "tenant-1", "corr-123"); + + Assert.Equal("user-1", request.SubjectId); + Assert.Equal("tenant-1", request.TenantId); + Assert.Equal("corr-123", request.CorrelationId); + } + + [Fact] + public void ThalosBffPackageContract_WhenCreated_UsesBlueprintDescriptorContract() + { + IBlueprintPackageContract contract = new ThalosBffPackageContract(); + + Assert.Equal("Thalos.Bff.Contracts", contract.Descriptor.PackageId); + Assert.Equal(PackageVersionPolicy.Minor, contract.Descriptor.VersionPolicy); + Assert.Contains("Core.Blueprint.Common", contract.Descriptor.DependencyPackageIds); + Assert.Contains("Thalos.Service.Identity.Abstractions", contract.Descriptor.DependencyPackageIds); + } +} diff --git a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs index b5bf048..ffbcf0f 100644 --- a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs @@ -1,7 +1,9 @@ +using Thalos.Bff.Application.Contracts; using Thalos.Bff.Application.Adapters; using Thalos.Bff.Application.Handlers; using Thalos.Bff.Application.Security; using Thalos.Bff.Contracts.Api; +using Thalos.Service.Identity.Abstractions.Contracts; namespace Thalos.Bff.Application.UnitTests; @@ -10,9 +12,12 @@ public class IssueTokenHandlerTests [Fact] public async Task HandleAsync_WhenPermissionAllowed_DelegatesToServiceClient() { - var handler = new IssueTokenHandler(new FakeThalosServiceClient(), new AllowPermissionGuard()); + var handler = new IssueTokenHandler( + new FakeThalosServiceClient(), + new FakeIdentityEdgeContractAdapter(), + new AllowPermissionGuard()); - var response = await handler.HandleAsync(new IssueTokenApiRequest("user-1", "tenant-1")); + var response = await handler.HandleAsync(new IssueTokenApiRequest("user-1", "tenant-1", "corr-123")); Assert.Equal("token-xyz", response.AccessToken); Assert.Equal(1800, response.ExpiresInSeconds); @@ -20,19 +25,52 @@ public class IssueTokenHandlerTests private sealed class FakeThalosServiceClient : IThalosServiceClient { - public Task IssueTokenAsync(IssueTokenApiRequest request) + public Task IssueTokenAsync(IssueIdentityTokenRequest request) { - return Task.FromResult(new IssueTokenApiResponse("token-xyz", 1800)); + return Task.FromResult(new IssueIdentityTokenResponse("token-xyz", 1800)); } - public Task RefreshSessionAsync(RefreshSessionApiRequest request) + public Task EvaluatePolicyAsync(EvaluateIdentityPolicyRequest request) { - return Task.FromResult(new RefreshSessionApiResponse("token-refreshed", 1800)); + return Task.FromResult(new EvaluateIdentityPolicyResponse(request.SubjectId, request.PermissionCode, true)); + } + + public Task RefreshSessionAsync(RefreshIdentitySessionRequest request) + { + return Task.FromResult(new RefreshIdentitySessionResponse("token-refreshed", 1800)); + } + } + + private sealed class FakeIdentityEdgeContractAdapter : IIdentityEdgeContractAdapter + { + public EvaluateIdentityPolicyRequest ToPolicyRequest(IssueTokenApiRequest request, string permissionCode) + { + return new EvaluateIdentityPolicyRequest(request.SubjectId, request.TenantId, permissionCode); + } + + public IssueIdentityTokenRequest ToIssueTokenRequest(IssueTokenApiRequest request) + { + return new IssueIdentityTokenRequest(request.SubjectId, request.TenantId); + } + + public IssueTokenApiResponse ToIssueTokenApiResponse(IssueIdentityTokenResponse response) + { + return new IssueTokenApiResponse(response.Token, response.ExpiresInSeconds); + } + + public RefreshIdentitySessionRequest ToRefreshSessionRequest(RefreshSessionApiRequest request) + { + return new RefreshIdentitySessionRequest(request.RefreshToken, request.CorrelationId); + } + + public RefreshSessionApiResponse ToRefreshSessionApiResponse(RefreshIdentitySessionResponse response) + { + return new RefreshSessionApiResponse(response.Token, response.ExpiresInSeconds); } } private sealed class AllowPermissionGuard : IPermissionGuard { - public bool CanAccess(string permissionCode) => true; + public bool CanAccess(EvaluateIdentityPolicyResponse policyResponse) => policyResponse.IsAllowed; } } diff --git a/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs new file mode 100644 index 0000000..aa494ac --- /dev/null +++ b/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs @@ -0,0 +1,67 @@ +using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.Contracts; +using Thalos.Bff.Application.Handlers; +using Thalos.Bff.Contracts.Api; +using Thalos.Service.Identity.Abstractions.Contracts; + +namespace Thalos.Bff.Application.UnitTests; + +public class RefreshSessionHandlerTests +{ + [Fact] + public async Task HandleAsync_WhenCalled_MapsThroughContractAdapter() + { + var handler = new RefreshSessionHandler(new FakeThalosServiceClient(), new FakeIdentityEdgeContractAdapter()); + + var response = await handler.HandleAsync(new RefreshSessionApiRequest("refresh-123", "corr-123")); + + Assert.Equal("token-refreshed", response.AccessToken); + Assert.Equal(1800, response.ExpiresInSeconds); + } + + private sealed class FakeThalosServiceClient : IThalosServiceClient + { + public Task IssueTokenAsync(IssueIdentityTokenRequest request) + { + return Task.FromResult(new IssueIdentityTokenResponse("token-xyz", 1800)); + } + + public Task EvaluatePolicyAsync(EvaluateIdentityPolicyRequest request) + { + return Task.FromResult(new EvaluateIdentityPolicyResponse(request.SubjectId, request.PermissionCode, true)); + } + + public Task RefreshSessionAsync(RefreshIdentitySessionRequest request) + { + return Task.FromResult(new RefreshIdentitySessionResponse("token-refreshed", 1800)); + } + } + + private sealed class FakeIdentityEdgeContractAdapter : IIdentityEdgeContractAdapter + { + public EvaluateIdentityPolicyRequest ToPolicyRequest(IssueTokenApiRequest request, string permissionCode) + { + return new EvaluateIdentityPolicyRequest(request.SubjectId, request.TenantId, permissionCode); + } + + public IssueIdentityTokenRequest ToIssueTokenRequest(IssueTokenApiRequest request) + { + return new IssueIdentityTokenRequest(request.SubjectId, request.TenantId); + } + + public IssueTokenApiResponse ToIssueTokenApiResponse(IssueIdentityTokenResponse response) + { + return new IssueTokenApiResponse(response.Token, response.ExpiresInSeconds); + } + + public RefreshIdentitySessionRequest ToRefreshSessionRequest(RefreshSessionApiRequest request) + { + return new RefreshIdentitySessionRequest(request.RefreshToken, request.CorrelationId); + } + + public RefreshSessionApiResponse ToRefreshSessionApiResponse(RefreshIdentitySessionResponse response) + { + return new RefreshSessionApiResponse(response.Token, response.ExpiresInSeconds); + } + } +}