Skip to content

Commit 6610aac

Browse files
authored
Get order by id endpoint (#98)
* Get order by id endpoint * fix
1 parent 848ddea commit 6610aac

35 files changed

+854
-47
lines changed

src/BuildingBlocks/BuildingBlocks/BuildingBlocks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="AspNetCore.HealthChecks.NpgSql"/>
1111
<PackageReference Include="AspNetCore.HealthChecks.UI.Client"/>
12+
<PackageReference Include="FastEndpoints" />
1213
<PackageReference Include="FluentValidation"/>
1314
<PackageReference Include="FluentValidation.AspNetCore"/>
1415
<PackageReference Include="FluentValidation.DependencyInjectionExtensions"/>

src/BuildingBlocks/BuildingBlocks/Exceptions/Handler/ExceptionHandlerMiddleware.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
using FluentValidation;
1+
using FastEndpoints;
2+
using FluentValidation;
23
using Microsoft.AspNetCore.Builder;
34
using Microsoft.AspNetCore.Diagnostics;
45
using Microsoft.AspNetCore.Http;
5-
using Microsoft.AspNetCore.Mvc;
66
using Serilog;
7+
using ProblemDetails = Microsoft.AspNetCore.Mvc.ProblemDetails;
78

89
namespace BuildingBlocks.Exceptions.Handler;
910

@@ -55,6 +56,19 @@ IExceptionHandlerFeature exHandlerFeature
5556
Title = error.GetType().Name,
5657
Status = StatusCodes.Status500InternalServerError
5758
},
59+
ValidationFailureException exception => new ProblemDetails
60+
{
61+
Detail = exception.Message,
62+
Title = exception.GetType().Name,
63+
Status = StatusCodes.Status400BadRequest,
64+
Extensions = new Dictionary<string, object?>
65+
{
66+
{
67+
"ValidationErrors", exception?.Failures?.Select(x =>
68+
new InvalidPropertyResponse(x.PropertyName, x.ErrorMessage)).ToArray() ?? []
69+
}
70+
}
71+
},
5872
ValidationException exception => new ProblemDetails()
5973
{
6074
Detail = exception.Message,

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
</PackageVersion>
4343
<PackageVersion Include="AspNetCore.HealthChecks.NpgSql" Version="8.0.0" />
4444
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.0" />
45-
<PackageVersion Include="FluentValidation" Version="11.9.0" />
45+
<PackageVersion Include="FluentValidation" Version="12.0.0" />
4646
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
4747
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
4848
<PackageVersion Include="Serilog" Version="3.1.1" />

src/Services/Identity/Identity.API/Config.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ private static class Policies
2626
public const string OrdersCommandCanGetOrdersListsByOrderName = "orders_command:order:all-by-order-name";
2727
public const string OrdersCommandCanUpdateOrder = "orders_command:order:update";
2828

29+
// Define policies for orders query operations
30+
public const string OrdersQueryCanGetOrder = "orders_query:order:get";
31+
public const string OrdersQueryCanGetAllOrders = "orders_query:order:all";
32+
public const string OrdersQueryCanGetOrdersListsByCustomerId = "orders_query:order:all-by-customer-id";
33+
public const string OrdersQueryCanGetOrdersListsByOrderName = "orders_query:order:all-by-order-name";
2934
}
3035
public static class RolePolicyDefinitions
3136
{
@@ -41,11 +46,16 @@ public static class RolePolicyDefinitions
4146

4247
{ Policies.OrdersCommandCanCreateOrder, [ Roles.Customer] },
4348
{ Policies.OrdersCommandCanDeleteOrder, [Roles.Admin] },
44-
{ Policies.OrdersCommandCanGetOrder, [Roles.Admin] },
49+
{ Policies.OrdersCommandCanGetOrder, [Roles.Admin, Roles.Customer] },
4550
{ Policies.OrdersCommandCanGetAllOrders, [Roles.Admin] },
4651
{ Policies.OrdersCommandCanGetOrdersListsByCustomerId, [Roles.Admin, Roles.Customer] },
4752
{ Policies.OrdersCommandCanGetOrdersListsByOrderName, [Roles.Admin] },
48-
{ Policies.OrdersCommandCanUpdateOrder, [Roles.Admin, Roles.Customer] }
53+
{ Policies.OrdersCommandCanUpdateOrder, [Roles.Admin, Roles.Customer] },
54+
55+
{ Policies.OrdersQueryCanGetOrder, [Roles.Admin, Roles.Customer] },
56+
{ Policies.OrdersQueryCanGetAllOrders, [Roles.Admin] },
57+
{ Policies.OrdersQueryCanGetOrdersListsByCustomerId, [Roles.Admin, Roles.Customer] },
58+
{ Policies.OrdersQueryCanGetOrdersListsByOrderName, [Roles.Admin] },
4959
};
5060
}
5161

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
using Marten;
12
using Microsoft.AspNetCore.Hosting;
23
using Microsoft.AspNetCore.Mvc.Testing;
34
using Microsoft.AspNetCore.TestHost;
5+
using Microsoft.Extensions.DependencyInjection;
46
using Order.Query.Api;
7+
using Order.Query.API.IntegrationTests.Given.DbGiven;
8+
using Order.Query.Events;
9+
using Order.Query.Features.OrderView;
10+
using WebMotions.Fake.Authentication.JwtBearer;
511

612
namespace Order.Query.API.IntegrationTests;
713

@@ -11,16 +17,75 @@ public class TestCollection : ICollectionFixture<ApiFactory>
1117
public const string Name = "TestCollection";
1218
}
1319

14-
public class ApiFactory : WebApplicationFactory<Program>
20+
public class ApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
1521
{
1622
private readonly CancellationTokenSource _timeoutCancellationTokenSource = new(TimeSpan.FromSeconds(30));
23+
private readonly WebApiContainerFactory _webApiContainerFactory = new();
1724
protected override void ConfigureWebHost(IWebHostBuilder builder)
1825
{
26+
var postgresConnectionString = _webApiContainerFactory.PostgresConnectionString;
27+
1928
builder.ConfigureTestServices(services =>
2029
{
21-
30+
services.AddMarten(options =>
31+
{
32+
options.Connection(postgresConnectionString);
33+
options.UseSystemTextJsonForSerialization();
34+
options.Schema.For<OrderView>()
35+
.Index(x => x.CustomerId)
36+
.Index(x => x.OrderStatus)
37+
.Index(x => x.TotalPrice)
38+
.Index(x => x.CreatedAt)
39+
.FullTextIndex(x => x.OrderName!)
40+
.UniqueIndex(x => x.Id);
41+
42+
options.Schema.For<EventStream>()
43+
.Index(x => x.Id)
44+
.Index(x => x.ViewId)
45+
.Index(x => x.EventType)
46+
.Index(x => x.CreatedAt)
47+
.UniqueIndex(x => x.Id);
48+
}).UseLightweightSessions();
49+
50+
services
51+
.AddAuthentication(FakeJwtBearerDefaults.AuthenticationScheme)
52+
.AddFakeJwtBearer();
2253
});
2354
}
2455

56+
private HttpClient? _httpClient;
57+
internal HttpClient HttpClient
58+
{
59+
get
60+
{
61+
_httpClient ??= CreateClient();
62+
return _httpClient;
63+
}
64+
}
65+
66+
private DbGiven? _sqlGiven;
67+
private IDocumentStore? _store;
68+
69+
internal IDocumentStore GetDocumentStore
70+
{
71+
get
72+
{
73+
_store ??= Services.GetRequiredService<IDocumentStore>();
74+
return _store;
75+
}
76+
}
77+
78+
internal DbGiven DbGiven => _sqlGiven ??= new DbGiven(GetDocumentStore);
79+
2580
internal CancellationToken CancellationToken => _timeoutCancellationTokenSource.Token;
81+
public async Task InitializeAsync()
82+
{
83+
await _webApiContainerFactory.InitializeAsync();
84+
}
85+
86+
public new async Task DisposeAsync()
87+
{
88+
await _webApiContainerFactory.DisposeAsync();
89+
HttpClient.Dispose();
90+
}
2691
}

src/Services/Order.Query/Order.Query.API.IntegrationTests/Features/GetOrderById/GetOrderByIdTests.cs

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
using System.Net;
22
using FastEndpoints;
3+
using IntegrationTests.Common;
4+
using Order.Query.Api.Authorization;
35
using Order.Query.API.Features.GetOrderById;
46
using Order.Query.API.IntegrationTests.AutoFixture;
7+
using Order.Query.API.IntegrationTests.Given.DbGiven;
8+
using ProblemDetails = Microsoft.AspNetCore.Mvc.ProblemDetails;
59
using Shouldly;
610

711
namespace Order.Query.API.IntegrationTests.Features.GetOrderById;
@@ -10,23 +14,122 @@ namespace Order.Query.API.IntegrationTests.Features.GetOrderById;
1014
public class GetOrderByIdTests(ApiFactory apiFactory) : IAsyncLifetime
1115
{
1216
private HttpClient _client = default!;
13-
17+
private DbGiven _dbGiven;
1418
public async Task InitializeAsync()
1519
{
1620
_client = apiFactory.CreateClient();
21+
_dbGiven = apiFactory.DbGiven;
22+
1723
await Task.CompletedTask;
1824
}
1925

2026
[Theory]
2127
[DomainDataAuto]
22-
public async Task GetOrderById_WhenNoTokenProvided_ReturnsUnauthorized(Request request)
28+
public async Task GetOrderById_WhenInvalidOrderId_ShouldReturnBadRequest(Request request)
29+
{
30+
// Arrange
31+
request = request with { OrderId = "invalid-order-id" };
32+
33+
// Act
34+
var (response, error) = await _client
35+
.SetFakeBearerToken(FakePermission.GetPermissions(
36+
[Policies.OrdersQueryCanGetOrderPermission]))
37+
.GETAsync<Endpoint, Request, ProblemDetails>(request);
38+
39+
// Assert
40+
response.ShouldNotBeNull();
41+
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
42+
43+
error.Detail!.ShouldContain("invalid-order-id is not a valid Ulid");
44+
}
45+
46+
[Theory]
47+
[DomainDataAuto]
48+
public async Task GetOrderById_WhenInvalidCustomerId_ShouldReturnBadRequest(Request request)
49+
{
50+
// Arrange
51+
request = request with { CustomerId = "invalid-customer-id" };
52+
53+
// Act
54+
var (response, error) = await _client
55+
.SetFakeBearerToken(FakePermission.GetPermissions(
56+
[Policies.OrdersQueryCanGetOrderPermission]))
57+
.GETAsync<Endpoint, Request, ProblemDetails>(request);
58+
59+
// Assert
60+
response.ShouldNotBeNull();
61+
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
62+
63+
error.Detail!.ShouldContain("invalid-customer-id is not a valid UUID");
64+
}
65+
66+
[Theory]
67+
[DomainDataAuto]
68+
public async Task GetOrderById_WhenNoToken_ShouldReturnUnauthorized(Request request)
69+
{
70+
// Act
71+
var response = await _client
72+
.GETAsync<Endpoint, Request>(request);
73+
74+
// Assert
75+
response.ShouldNotBeNull();
76+
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
77+
}
78+
79+
[Theory]
80+
[DomainDataAuto]
81+
public async Task GetOrderById_WhenNoPermission_ShouldReturnForbidden(Request request)
82+
{
83+
// Act
84+
var response = await _client
85+
.SetFakeBearerToken(FakePermission.GetPermissions(
86+
[], sub: request.CustomerId))
87+
.GETAsync<Endpoint, Request>(request);
88+
89+
// Assert
90+
response.ShouldNotBeNull();
91+
response.StatusCode.ShouldBe(HttpStatusCode.Forbidden);
92+
}
93+
94+
[Theory]
95+
[DomainDataAuto]
96+
public async Task GetOrderById_WhenHavingPermissionButWrongUser_ShouldReturnForbidden(Request request)
97+
{
98+
// Act
99+
var response = await _client
100+
.SetFakeBearerToken(FakePermission.GetPermissions(
101+
[Policies.OrdersQueryCanGetOrderPermission]))
102+
.GETAsync<Endpoint, Request>(request);
103+
104+
// Assert
105+
response.ShouldNotBeNull();
106+
response.StatusCode.ShouldBe(HttpStatusCode.Forbidden);
107+
}
108+
109+
[Theory]
110+
[DomainDataAuto]
111+
public async Task GetOrderById_WhenExistingOrderId_ShouldReturnExpectedResponse(Request request)
23112
{
113+
// Arrange
114+
await _dbGiven.OrderViewConfigurationGiven(x =>
115+
{
116+
x.Id = request.OrderId!;
117+
x.CustomerId = request.CustomerId!;
118+
});
119+
24120
// Act
25-
var response = await _client.GETAsync<Endpoint, Request>(request);
121+
var (response, result) = await _client
122+
.SetFakeBearerToken(FakePermission.GetPermissions(
123+
[Policies.OrdersQueryCanGetOrderPermission], sub: request.CustomerId))
124+
.GETAsync<Endpoint, Request, Response>(request);
26125

27126
// Assert
28127
response.ShouldNotBeNull();
29128
response.StatusCode.ShouldBe(HttpStatusCode.OK);
129+
130+
result.ShouldNotBeNull();
131+
result.Id.ShouldBe(result.Id);
132+
result.CustomerId.ShouldBe(result.CustomerId);
30133
}
31134

32135
public async Task DisposeAsync()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Marten;
2+
using Order.Query.Features.OrderView;
3+
4+
namespace Order.Query.API.IntegrationTests.Given.DbGiven;
5+
6+
public class DbGiven(IDocumentStore store)
7+
{
8+
public async Task OrderViewConfigurationGiven(Action<OrderViewConfiguration>? configuration = null)
9+
{
10+
var orderView = new OrderViewConfiguration();
11+
configuration?.Invoke(orderView);
12+
13+
await using var session = store.LightweightSession();
14+
session.Store<OrderView>(orderView.ToDbModel());
15+
await session.SaveChangesAsync();
16+
}
17+
}

0 commit comments

Comments
 (0)