Skip to content

Commit

Permalink
Automatically Set CreatedAt and UpdatedAt (#244)
Browse files Browse the repository at this point in the history
* Create IHasCreationTimestamp interface.

* Automatically set creation timestamps.

* Implement IHasCreationTimestamp.

* Don't manually set CreatedAt.

* Map all Category properties  on creation.

* Fix up tests.

* Don't override Equals or GetHashCode.

* Create IHasUpdateTimestamp.

* Implement IHasUpdateTimestamp.

* Automatically set update timestamp.

* Simplify clock initialization.

* Don't manually set LB CreatedAt in test.

* Remove erroneous readonly modifiers.
  • Loading branch information
TheTedder authored Sep 22, 2024
1 parent 826bb2e commit 4021aff
Show file tree
Hide file tree
Showing 19 changed files with 207 additions and 149 deletions.
47 changes: 33 additions & 14 deletions LeaderboardBackend.Test/Categories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
using LeaderboardBackend.Models.ViewModels;
using LeaderboardBackend.Test.TestApi;
using LeaderboardBackend.Test.TestApi.Extensions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using NodaTime.Testing;
using NUnit.Framework;

namespace LeaderboardBackend.Test;
Expand All @@ -14,16 +19,23 @@ namespace LeaderboardBackend.Test;
internal class Categories
{
private static TestApiClient _apiClient = null!;
private static TestApiFactory _factory = null!;
private static WebApplicationFactory<Program> _factory = null!;
private static readonly FakeClock _clock = new(new Instant());
private static string? _jwt;

[OneTimeSetUp]
public async Task OneTimeSetUp()
{
_factory = new TestApiFactory();
_apiClient = _factory.CreateTestApiClient();
_factory = new TestApiFactory().WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<IClock, FakeClock>(_ => _clock);
});
});
_apiClient = new TestApiClient(_factory.CreateClient());

_factory.ResetDatabase();
PostgresDatabaseFixture.ResetDatabaseToTemplate();
_jwt = (await _apiClient.LoginAdminUser()).Token;
}

Expand All @@ -44,6 +56,9 @@ await _apiClient.Awaiting(
[Test]
public static async Task CreateCategory_GetCategory_OK()
{
Instant now = Instant.FromUnixTimeSeconds(1);
_clock.Reset(now);

LeaderboardViewModel createdLeaderboard = await _apiClient.Post<LeaderboardViewModel>(
"/leaderboards/create",
new()
Expand All @@ -58,27 +73,31 @@ public static async Task CreateCategory_GetCategory_OK()
}
);

CreateCategoryRequest request = new()
{
Name = "1 Player",
Slug = "1_player",
LeaderboardId = createdLeaderboard.Id,
Info = null,
SortDirection = SortDirection.Ascending,
Type = RunType.Time
};

CategoryViewModel createdCategory = await _apiClient.Post<CategoryViewModel>(
"/categories/create",
new()
{
Body = new CreateCategoryRequest()
{
Name = "1 Player",
Slug = "1_player",
LeaderboardId = createdLeaderboard.Id,
Info = null,
SortDirection = SortDirection.Ascending,
Type = RunType.Time
},
Body = request,
Jwt = _jwt
}
);

createdCategory.CreatedAt.Should().Be(now);

CategoryViewModel retrievedCategory = await _apiClient.Get<CategoryViewModel>(
$"/api/category/{createdCategory?.Id}", new() { }
);

Assert.AreEqual(createdCategory, retrievedCategory);
retrievedCategory.Should().BeEquivalentTo(request, opts => opts.Excluding(c => c.LeaderboardId));
}
}
49 changes: 29 additions & 20 deletions LeaderboardBackend.Test/Features/Users/ConfirmAccountTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ public class ConfirmAccountTests : IntegrationTestsBase
[SetUp]
public void Init()
{
_scope = _factory.Services.CreateScope();
_scope = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<IClock, FakeClock>(_ => _clock);
});
}).Services.CreateScope();

_client = _factory.WithWebHostBuilder(builder =>
{
Expand All @@ -44,12 +50,12 @@ public void TearDown()
[Test]
public async Task ConfirmAccount_BadConfirmationId()
{
_clock.Reset(Instant.FromUnixTimeSeconds(1));
Instant now = Instant.FromUnixTimeSeconds(1);
_clock.Reset(now);
ApplicationContext context = _scope.ServiceProvider.GetRequiredService<ApplicationContext>();
AccountConfirmation confirmation = new()
{
CreatedAt = Instant.FromUnixTimeSeconds(0),
ExpiresAt = Instant.FromUnixTimeSeconds(0).Plus(Duration.FromHours(1)),
ExpiresAt = now.Plus(Duration.FromHours(1)),
User = new()
{
Email = "[email protected]",
Expand All @@ -60,6 +66,7 @@ public async Task ConfirmAccount_BadConfirmationId()

context.AccountConfirmations.Add(confirmation);
await context.SaveChangesAsync();
confirmation.CreatedAt.Should().Be(now);
HttpResponseMessage res = await _client.PutAsync(Routes.ConfirmAccount(Guid.NewGuid()), null);
res.StatusCode.Should().Be(HttpStatusCode.NotFound);
context.ChangeTracker.Clear();
Expand All @@ -78,13 +85,12 @@ public async Task ConfirmAccount_MalformedConfirmationId()
[Test]
public async Task ConfirmAccount_BadRole()
{
_clock.Reset(Instant.FromUnixTimeSeconds(1));
Instant now = Instant.FromUnixTimeSeconds(1);
_clock.Reset(now);
ApplicationContext context = _scope.ServiceProvider.GetRequiredService<ApplicationContext>();

AccountConfirmation confirmation = new()
{
CreatedAt = Instant.FromUnixTimeSeconds(0),
ExpiresAt = Instant.FromUnixTimeSeconds(0).Plus(Duration.FromHours(1)),
ExpiresAt = now.Plus(Duration.FromHours(1)),
User = new()
{
Email = "[email protected]",
Expand All @@ -96,7 +102,7 @@ public async Task ConfirmAccount_BadRole()

context.AccountConfirmations.Add(confirmation);
await context.SaveChangesAsync();

confirmation.CreatedAt.Should().Be(now);
HttpResponseMessage res = await _client.PutAsync(Routes.ConfirmAccount(confirmation.Id), null);
res.StatusCode.Should().Be(HttpStatusCode.Conflict);
context.ChangeTracker.Clear();
Expand All @@ -107,13 +113,13 @@ public async Task ConfirmAccount_BadRole()
[Test]
public async Task ConfirmAccount_Expired()
{
_clock.Reset(Instant.FromUnixTimeSeconds(1) + Duration.FromHours(1));
Instant now = Instant.FromUnixTimeSeconds(1);
_clock.Reset(now);
ApplicationContext context = _scope.ServiceProvider.GetRequiredService<ApplicationContext>();

AccountConfirmation confirmation = new()
{
CreatedAt = Instant.FromUnixTimeSeconds(0),
ExpiresAt = Instant.FromUnixTimeSeconds(0).Plus(Duration.FromHours(1)),
ExpiresAt = now.Plus(Duration.FromHours(1)),
User = new()
{
Email = "[email protected]",
Expand All @@ -124,6 +130,7 @@ public async Task ConfirmAccount_Expired()

context.AccountConfirmations.Add(confirmation);
await context.SaveChangesAsync();
_clock.Reset(now + Duration.FromHours(2));
HttpResponseMessage res = await _client.PutAsync(Routes.ConfirmAccount(confirmation.Id), null);
res.StatusCode.Should().Be(HttpStatusCode.NotFound);
context.ChangeTracker.Clear();
Expand All @@ -135,14 +142,14 @@ public async Task ConfirmAccount_Expired()
[Test]
public async Task ConfirmAccount_AlreadyUsed()
{
_clock.Reset(Instant.FromUnixTimeSeconds(1));
Instant now = Instant.FromUnixTimeSeconds(1);
_clock.Reset(now);
ApplicationContext context = _scope.ServiceProvider.GetRequiredService<ApplicationContext>();

AccountConfirmation confirmation = new()
{
CreatedAt = Instant.FromUnixTimeSeconds(0),
ExpiresAt = Instant.FromUnixTimeSeconds(0).Plus(Duration.FromHours(1)),
UsedAt = Instant.FromUnixTimeSeconds(5),
ExpiresAt = now.Plus(Duration.FromHours(1)),
UsedAt = now.Plus(Duration.FromSeconds(5)),
User = new()
{
Email = "[email protected]",
Expand All @@ -153,6 +160,7 @@ public async Task ConfirmAccount_AlreadyUsed()

context.AccountConfirmations.Add(confirmation);
await context.SaveChangesAsync();
_clock.AdvanceMinutes(1);
HttpResponseMessage res = await _client.PutAsync(Routes.ConfirmAccount(confirmation.Id), null);
res.StatusCode.Should().Be(HttpStatusCode.NotFound);
context.ChangeTracker.Clear();
Expand All @@ -163,12 +171,12 @@ public async Task ConfirmAccount_AlreadyUsed()
[Test]
public async Task ConfirmAccount_Success()
{
_clock.Reset(Instant.FromUnixTimeSeconds(1));
Instant now = Instant.FromUnixTimeSeconds(1);
_clock.Reset(now);

AccountConfirmation confirmation = new()
{
CreatedAt = Instant.FromUnixTimeSeconds(0),
ExpiresAt = Instant.FromUnixTimeSeconds(0).Plus(Duration.FromHours(1)),
ExpiresAt = now.Plus(Duration.FromHours(1)),
User = new()
{
Email = "[email protected]",
Expand All @@ -180,11 +188,12 @@ public async Task ConfirmAccount_Success()
ApplicationContext context = _scope.ServiceProvider.GetRequiredService<ApplicationContext>();
context.AccountConfirmations.Add(confirmation);
await context.SaveChangesAsync();
_clock.AdvanceMinutes(5);
HttpResponseMessage res = await _client.PutAsync(Routes.ConfirmAccount(confirmation.Id), null);
res.Should().HaveStatusCode(HttpStatusCode.OK);
context.ChangeTracker.Clear();
AccountConfirmation? conf = await context.AccountConfirmations.Include(c => c.User).SingleOrDefaultAsync(c => c.Id == confirmation.Id);
conf!.UsedAt.Should().Be(Instant.FromUnixTimeSeconds(1));
conf!.UsedAt.Should().Be(now.Plus(Duration.FromMinutes(5)));
conf!.User.Role.Should().Be(UserRole.Confirmed);
}
}
16 changes: 12 additions & 4 deletions LeaderboardBackend.Test/Features/Users/RegistrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,33 @@ public class RegistrationTests : IntegrationTestsBase
public async Task Register_ValidRequest_CreatesAndReturnsUser()
{
Mock<IEmailSender> emailSenderMock = new();
Instant now = Instant.FromUnixTimeSeconds(1);

using HttpClient client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped(_ => emailSenderMock.Object);
services.AddSingleton<IClock, FakeClock>(_ => new(Instant.FromUnixTimeSeconds(1)));
services.AddSingleton<IClock, FakeClock>(_ => new(now));
});
})
.CreateClient();

RegisterRequest request = _registerReqFaker.Generate();

HttpResponseMessage res = await client.PostAsJsonAsync(Routes.REGISTER, request);

res.Should().HaveStatusCode(HttpStatusCode.Created);
UserViewModel? content = await res.Content.ReadFromJsonAsync<UserViewModel>(TestInitCommonFields.JsonSerializerOptions);
content.Should().NotBeNull().And.BeEquivalentTo(new UserViewModel

content.Should().NotBeNull().And.Be(new UserViewModel
{
Id = content!.Id,
Username = request.Username,
Role = UserRole.Registered,
CreatedAt = Instant.FromUnixTimeSeconds(1)
CreatedAt = now
});

emailSenderMock.Verify(x =>
x.EnqueueEmailAsync(
It.IsAny<string>(),
Expand All @@ -66,14 +71,17 @@ public async Task Register_ValidRequest_CreatesAndReturnsUser()
using IServiceScope scope = _factory.Services.CreateScope();
using ApplicationContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
User? createdUser = dbContext.Users.FirstOrDefault(u => u.Id == content.Id);

createdUser.Should().NotBeNull().And.BeEquivalentTo(new User
{
Id = content!.Id,
Password = createdUser!.Password,
Username = request.Username,
Email = request.Email,
Role = UserRole.Registered
Role = UserRole.Registered,
CreatedAt = now
});

AccountConfirmation confirmation = dbContext.AccountConfirmations.First(c => c.UserId == createdUser.Id);
confirmation.Should().NotBeNull();
confirmation.CreatedAt.ToUnixTimeSeconds().Should().Be(1);
Expand Down
Loading

0 comments on commit 4021aff

Please sign in to comment.