From ccdb62762ead3b2cc32dcac10a185dc63264769f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 13 Feb 2025 13:02:42 +0100 Subject: [PATCH] Fix date handling timezone bug with PostgreSQL (#541) --- .../AliasServerDbContextPostgresql.cs | 4 +- .../Tests/Client/Shard1/ApiLoggingTests.cs | 44 ---------- .../Tests/Client/Shard1/ApiTests.cs | 86 +++++++++++++++++++ 3 files changed, 88 insertions(+), 46 deletions(-) delete mode 100644 src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiLoggingTests.cs create mode 100644 src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiTests.cs diff --git a/src/Databases/AliasServerDb/AliasServerDbContextPostgresql.cs b/src/Databases/AliasServerDb/AliasServerDbContextPostgresql.cs index e17a9224..c3cbddfb 100644 --- a/src/Databases/AliasServerDb/AliasServerDbContextPostgresql.cs +++ b/src/Databases/AliasServerDb/AliasServerDbContextPostgresql.cs @@ -74,8 +74,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // Add value converter for DateTime properties var converter = new ValueConverter( - v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(), - v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + v => v.ToUniversalTime(), + v => v.ToUniversalTime()); property.SetValueConverter(converter); } diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiLoggingTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiLoggingTests.cs deleted file mode 100644 index a94c7478..00000000 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiLoggingTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) lanedirt. All rights reserved. -// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. -// -//----------------------------------------------------------------------- - -namespace AliasVault.E2ETests.Tests.Client.Shard1; - -using Microsoft.EntityFrameworkCore; - -/// -/// End-to-end tests for making sure errors and warnings in API are logged to database. -/// -[TestFixture] -[Category("ClientTests")] -[NonParallelizable] -public class ApiLoggingTests : ClientPlaywrightTest -{ - /// - /// Test if an error in the API is logged to the database. - /// - /// Async task. - [Test] - public async Task ApiDbLogTest() - { - // Call webapi endpoint that throws an exception. - try - { - await Page.GotoAsync(ApiBaseUrl + "v1/Test/Error"); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - } - catch - { - // Ignore exception as this is expected. - } - - // Read from database to check if the log entry was created. - var logEntry = await ApiDbContext.Logs.Where(x => x.Application == "AliasVault.Api").OrderByDescending(x => x.Id).FirstOrDefaultAsync(); - - Assert.That(logEntry, Is.Not.Null, "Log entry for triggered exception not found in database. Check Serilog configuration and /v1/Test/Error endpoint."); - Assert.That(logEntry.Exception, Does.Contain("Test error"), "Log entry in database does not contain expected message. Check exception and Serilog configuration."); - } -} diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiTests.cs new file mode 100644 index 00000000..a8a23581 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/ApiTests.cs @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.E2ETests.Tests.Client.Shard1; + +using AliasServerDb; +using Microsoft.EntityFrameworkCore; + +/// +/// End-to-end tests for making sure errors and warnings in API are logged to database. +/// +[TestFixture] +[Category("ClientTests")] +[NonParallelizable] +public class ApiTests : ClientPlaywrightTest +{ + /// + /// Test if an error in the API is logged to the database. + /// + /// Async task. + [Test] + public async Task ApiDbLogTest() + { + // Call webapi endpoint that throws an exception. + try + { + await Page.GotoAsync(ApiBaseUrl + "v1/Test/Error"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + catch + { + // Ignore exception as this is expected. + } + + // Read from database to check if the log entry was created. + var logEntry = await ApiDbContext.Logs.Where(x => x.Application == "AliasVault.Api").OrderByDescending(x => x.Id).FirstOrDefaultAsync(); + + Assert.That(logEntry, Is.Not.Null, "Log entry for triggered exception not found in database. Check Serilog configuration and /v1/Test/Error endpoint."); + Assert.That(logEntry.Exception, Does.Contain("Test error"), "Log entry in database does not contain expected message. Check exception and Serilog configuration."); + } + + /// + /// Test if dates are stored and retrieved correctly without unwanted timezone conversions. + /// + /// Async task. + [Test] + public async Task DateTimeStorageTest() + { + // Create a UTC date for testing. + var newTokenLifetime = TimeSpan.FromMinutes(5); + var testDate = DateTime.UtcNow.Add(newTokenLifetime); + + // Create a test refresh token with the fixed date. + var user = await ApiDbContext.AliasVaultUsers.FirstAsync(); + var refreshToken = new AliasVaultUserRefreshToken + { + Id = Guid.NewGuid(), + UserId = user.Id, + DeviceIdentifier = "test-device", + Value = "test-value", + ExpireDate = testDate, + CreatedAt = testDate, + }; + + // Add to database and save. + await ApiDbContext.AliasVaultUserRefreshTokens.AddAsync(refreshToken); + await ApiDbContext.SaveChangesAsync(); + + // Clear the DbContext's change tracker + ApiDbContext.ChangeTracker.Clear(); + + // Retrieve the token from database without tracking to ensure we get a fresh copy from the database. + var retrievedToken = await ApiDbContext.AliasVaultUserRefreshTokens + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == refreshToken.Id); + + // Assert dates match exactly. + Assert.That(retrievedToken, Is.Not.Null, "Refresh token not found in database"); + Assert.That(retrievedToken.ExpireDate, Is.EqualTo(testDate), "ExpireDate was modified during storage/retrieval"); + Assert.That(retrievedToken.CreatedAt, Is.EqualTo(testDate), "CreatedAt was modified during storage/retrieval"); + } +}