Skip to content

Commit caaa312

Browse files
committed
added integration test
1 parent 5c4add5 commit caaa312

17 files changed

Lines changed: 1826 additions & 9 deletions

src/eCommerce.Api/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@
5858
app.MapControllers();
5959

6060
app.Run();
61+
62+
// Exposes the top-level-statements entry point as a public type so the integration
63+
// test project can target it with WebApplicationFactory<Program>. Without this, the
64+
// generated Program class is internal and not referenceable from the test assembly.
65+
public partial class Program { }

src/eCommerce.Persistence/Repositories/CartRepository.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public CartRepository(AppDbContext context, ILogger<CartRepository> logger)
3030
catch (Exception ex)
3131
{
3232
_logger.LogError(ex, "{Repository}-{Method} - Error adding cart {Entity}", nameof(CartRepository), nameof(AddAsync), entity);
33-
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
33+
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
3434
throw;
3535
}
3636
}
@@ -46,7 +46,7 @@ public async Task DeleteAsync(Cart entity)
4646
catch (Exception ex)
4747
{
4848
_logger.LogError(ex, "{Repository}-{Method} - Error deleting cart {Entity}", nameof(CartRepository), nameof(DeleteAsync), entity);
49-
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
49+
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
5050
throw;
5151
}
5252
}
@@ -64,7 +64,7 @@ public async Task<IEnumerable<Cart>> FindAsync(Expression<Func<Cart, bool>> pred
6464
catch (Exception ex)
6565
{
6666
_logger.LogError(ex, "{Repository}-{Method} - Error finding carts with predicate {Predicate}", nameof(CartRepository), nameof(FindAsync), predicate);
67-
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
67+
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
6868
throw;
6969
}
7070
}
@@ -80,7 +80,7 @@ public async Task<IEnumerable<Cart>> GetAllAsync()
8080
catch (Exception ex)
8181
{
8282
_logger.LogError(ex, "{Repository}-{Method} - Error retrieving all carts", nameof(CartRepository), nameof(GetAllAsync));
83-
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
83+
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
8484
throw;
8585
}
8686
}
@@ -96,7 +96,7 @@ public async Task<IEnumerable<Cart>> GetAllAsync()
9696
catch (Exception ex)
9797
{
9898
_logger.LogError(ex, "{Repository}-{Method} - Error retrieving cart by id {Id}", nameof(CartRepository), nameof(GetByIdAsync), id);
99-
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
99+
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
100100
throw;
101101
}
102102
}
@@ -105,6 +105,15 @@ public async Task<IEnumerable<Cart>> GetAllAsync()
105105
{
106106
try
107107
{
108+
// The cart may already be tracked in this same DbContext (e.g. it was just created earlier in
109+
// the request). Attaching a second instance with the same key throws an identity conflict, so
110+
// detach the tracked copy first and let this instance take over.
111+
var tracked = _context.Carts.Local.FirstOrDefault(c => c.Id == entity.Id);
112+
if (tracked != null && !ReferenceEquals(tracked, entity))
113+
{
114+
_context.Entry(tracked).State = EntityState.Detached;
115+
}
116+
108117
_context.Carts.Update(entity);
109118
await _context.SaveChangesAsync();
110119
_logger.LogInformation("{Repository}-{Method} - Successfully updated cart {Entity}", nameof(CartRepository), nameof(UpdateAsync), entity);
@@ -113,11 +122,11 @@ public async Task<IEnumerable<Cart>> GetAllAsync()
113122
catch (Exception ex)
114123
{
115124
_logger.LogError(ex, "{Repository}-{Method} - Error updating cart {Entity}", nameof(CartRepository), nameof(UpdateAsync), entity);
116-
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
125+
_logger.LogCritical(ex, "{Repository} - Critical repository failure", nameof(CartRepository));
117126
throw;
118127
}
119128
}
120129
}
121-
122-
123-
130+
131+
132+
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using System.Text.Json;
4+
using eCommerce.Application.DTOs.Auth;
5+
using eCommerce.Test.Integration.Common;
6+
using FluentAssertions;
7+
8+
namespace eCommerce.Test.Integration;
9+
10+
/// <summary>
11+
/// End-to-end tests for /api/Account. These cover the public auth flows (register, login, refresh) and
12+
/// the protected, role- and ownership-gated account operations. Register and login are normally rate
13+
/// limited; the test host relaxes that so the flows can run freely.
14+
/// </summary>
15+
public class AccountControllerTests : IntegrationTestBase
16+
{
17+
public AccountControllerTests(CustomWebApplicationFactory factory) : base(factory)
18+
{
19+
}
20+
21+
// A password that satisfies the configured Identity policy (>=8 chars, upper, lower, digit).
22+
private const string ValidPassword = "Password1";
23+
24+
private static RegisterUserDto NewUser(string? email = null)
25+
{
26+
var unique = Guid.NewGuid().ToString("N");
27+
return new RegisterUserDto(
28+
FullName: "Test User",
29+
UserName: $"user_{unique}",
30+
Email: email ?? $"user_{unique}@test.com",
31+
Password: ValidPassword,
32+
PhoneNumber: null);
33+
}
34+
35+
/// <summary>Registers a user and returns the new account id (read from the response payload).</summary>
36+
private async Task<(Guid Id, RegisterUserDto Dto)> RegisterUserAsync(HttpClient client)
37+
{
38+
var dto = NewUser();
39+
var response = await client.PostAsJsonAsync("/api/Account/register", dto);
40+
response.StatusCode.Should().Be(HttpStatusCode.OK);
41+
42+
var result = await ReadAsAsync<RegisterResultDto>(response);
43+
result.Succeeded.Should().BeTrue();
44+
45+
// responseData is serialised as a JSON object; pull the new account id out of it.
46+
var id = ((JsonElement)result.responseData!).GetProperty("id").GetGuid();
47+
return (id, dto);
48+
}
49+
50+
[Fact]
51+
public async Task Register_WithValidData_ShouldSucceed()
52+
{
53+
var client = CreateAnonymousClient();
54+
55+
var response = await client.PostAsJsonAsync("/api/Account/register", NewUser());
56+
57+
response.StatusCode.Should().Be(HttpStatusCode.OK);
58+
var result = await ReadAsAsync<RegisterResultDto>(response);
59+
result.Succeeded.Should().BeTrue();
60+
}
61+
62+
[Fact]
63+
public async Task Register_WithDuplicateEmail_ShouldReturnBadRequest()
64+
{
65+
var client = CreateAnonymousClient();
66+
var dto = NewUser();
67+
await client.PostAsJsonAsync("/api/Account/register", dto);
68+
69+
// Registering the same email again conflicts; Conflict maps to 400 at the HTTP layer.
70+
var response = await client.PostAsJsonAsync("/api/Account/register", dto);
71+
72+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
73+
}
74+
75+
[Fact]
76+
public async Task Register_WithMissingFields_ShouldReturnBadRequest()
77+
{
78+
var client = CreateAnonymousClient();
79+
80+
// An empty body leaves required (non-nullable) fields null; [ApiController] auto-validation rejects it.
81+
var response = await client.PostAsJsonAsync("/api/Account/register", new { });
82+
83+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
84+
}
85+
86+
[Fact]
87+
public async Task Login_WithValidCredentials_ShouldReturnToken()
88+
{
89+
var client = CreateAnonymousClient();
90+
var (_, dto) = await RegisterUserAsync(client);
91+
92+
var response = await client.PostAsJsonAsync("/api/Account/login", new LoginDto(dto.Email, dto.Password));
93+
94+
response.StatusCode.Should().Be(HttpStatusCode.OK);
95+
var auth = await ReadAsAsync<AuthResponseDto>(response);
96+
auth.AccessToken.Should().NotBeNullOrWhiteSpace();
97+
}
98+
99+
[Fact]
100+
public async Task Login_WithWrongPassword_ShouldReturnUnauthorized()
101+
{
102+
var client = CreateAnonymousClient();
103+
var (_, dto) = await RegisterUserAsync(client);
104+
105+
var response = await client.PostAsJsonAsync("/api/Account/login", new LoginDto(dto.Email, "WrongPass1"));
106+
107+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
108+
}
109+
110+
[Fact]
111+
public async Task RefreshToken_WithValidToken_ShouldReturnNewToken()
112+
{
113+
// Arrange: register + login to obtain a real refresh token.
114+
var client = CreateAnonymousClient();
115+
var (_, dto) = await RegisterUserAsync(client);
116+
var login = await ReadAsAsync<AuthResponseDto>(
117+
await client.PostAsJsonAsync("/api/Account/login", new LoginDto(dto.Email, dto.Password)));
118+
119+
// Act: exchange the refresh token for a new access token.
120+
var response = await client.PostAsJsonAsync($"/api/Account/refresh-token/{login.UserId}", login);
121+
122+
// Assert
123+
response.StatusCode.Should().Be(HttpStatusCode.OK);
124+
var refreshed = await ReadAsAsync<AuthResponseDto>(response);
125+
refreshed.AccessToken.Should().NotBeNullOrWhiteSpace();
126+
}
127+
128+
[Fact]
129+
public async Task RefreshToken_WithInvalidToken_ShouldReturnUnauthorized()
130+
{
131+
var client = CreateAnonymousClient();
132+
var (id, _) = await RegisterUserAsync(client);
133+
var bogus = new AuthResponseDto("access", DateTime.UtcNow.AddMinutes(5), id, "not-a-real-refresh-token", DateTime.UtcNow.AddDays(1));
134+
135+
var response = await client.PostAsJsonAsync($"/api/Account/refresh-token/{id}", bogus);
136+
137+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
138+
}
139+
140+
[Fact]
141+
public async Task RegisterAdmin_AsSuperAdmin_ShouldSucceed()
142+
{
143+
var client = CreateSuperAdminClient();
144+
145+
var response = await client.PostAsJsonAsync("/api/Account/register-admin", NewUser());
146+
147+
response.StatusCode.Should().Be(HttpStatusCode.OK);
148+
}
149+
150+
[Fact]
151+
public async Task RegisterAdmin_AsUser_ShouldReturnForbidden()
152+
{
153+
var client = CreateUserClient(Guid.NewGuid());
154+
155+
var response = await client.PostAsJsonAsync("/api/Account/register-admin", NewUser());
156+
157+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
158+
}
159+
160+
[Fact]
161+
public async Task RegisterSuperAdmin_AsSuperAdmin_ShouldSucceed()
162+
{
163+
var client = CreateSuperAdminClient();
164+
165+
var response = await client.PostAsJsonAsync("/api/Account/register-superadmin", NewUser());
166+
167+
response.StatusCode.Should().Be(HttpStatusCode.OK);
168+
}
169+
170+
[Fact]
171+
public async Task ActivateAdmin_AsSuperAdmin_ShouldSucceed()
172+
{
173+
// Arrange: create an admin, then read its id back from the registration payload.
174+
var client = CreateSuperAdminClient();
175+
var register = await client.PostAsJsonAsync("/api/Account/register-admin", NewUser());
176+
var result = await ReadAsAsync<RegisterResultDto>(register);
177+
var adminId = ((JsonElement)result.responseData!).GetProperty("id").GetGuid();
178+
179+
// Act
180+
var response = await client.PutAsync($"/api/Account/activate/admin/{adminId}", content: null);
181+
182+
// Assert
183+
response.StatusCode.Should().Be(HttpStatusCode.OK);
184+
}
185+
186+
[Fact]
187+
public async Task ActivateAdmin_WhenMissing_ShouldReturnNotFound()
188+
{
189+
var client = CreateSuperAdminClient();
190+
191+
var response = await client.PutAsync($"/api/Account/activate/admin/{SeedIds.NonExistentId}", content: null);
192+
193+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
194+
}
195+
196+
[Fact]
197+
public async Task UpdateAccount_AsOwner_ShouldSucceed()
198+
{
199+
// Arrange: register a user, then act as that user to update their own account.
200+
var anonymous = CreateAnonymousClient();
201+
var (id, dto) = await RegisterUserAsync(anonymous);
202+
var client = CreateClientAs(id, dto.Email, UserRole);
203+
204+
// Act
205+
var response = await client.PutAsJsonAsync($"/api/Account/update-account/{id}",
206+
new UpdateAccountDto("Updated Name", "+1000000000"));
207+
208+
// Assert
209+
response.StatusCode.Should().Be(HttpStatusCode.OK);
210+
}
211+
212+
[Fact]
213+
public async Task UpdateAccount_ForAnotherUser_ShouldReturnUnauthorized()
214+
{
215+
// Arrange: signed in as one user, targeting another user's account without privilege.
216+
var caller = Guid.NewGuid();
217+
var client = CreateUserClient(caller);
218+
219+
var response = await client.PutAsJsonAsync($"/api/Account/update-account/{Guid.NewGuid()}",
220+
new UpdateAccountDto("Hacker", null));
221+
222+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
223+
}
224+
225+
[Fact]
226+
public async Task ChangePassword_AsOwner_ShouldSucceed()
227+
{
228+
var anonymous = CreateAnonymousClient();
229+
var (id, dto) = await RegisterUserAsync(anonymous);
230+
var client = CreateClientAs(id, dto.Email, UserRole);
231+
232+
var response = await client.PostAsJsonAsync($"/api/Account/change-password/{id}",
233+
new ChangePasswordDto(ValidPassword, "NewPassword2"));
234+
235+
response.StatusCode.Should().Be(HttpStatusCode.OK);
236+
}
237+
238+
[Fact]
239+
public async Task Logout_WhenAuthenticated_ShouldSucceed()
240+
{
241+
var client = CreateUserClient(Guid.NewGuid());
242+
243+
var response = await client.GetAsync("/api/Account/logout");
244+
245+
response.StatusCode.Should().Be(HttpStatusCode.OK);
246+
}
247+
}

0 commit comments

Comments
 (0)