diff --git a/.github/workflows/container_apps_chat_cd.yml b/.github/workflows/container_apps_chat_cd.yml index 7d8608c9..9585146d 100644 --- a/.github/workflows/container_apps_chat_cd.yml +++ b/.github/workflows/container_apps_chat_cd.yml @@ -18,7 +18,7 @@ jobs: - name: Set up dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.x" + dotnet-version: "9.x" - name: Test run: dotnet test "${{ env.TEST_DIRECTORY }}" --filter Category!=ManualTest diff --git a/.github/workflows/container_apps_chat_ci.yml b/.github/workflows/container_apps_chat_ci.yml index 44b95d0c..5029f5fb 100644 --- a/.github/workflows/container_apps_chat_ci.yml +++ b/.github/workflows/container_apps_chat_ci.yml @@ -21,7 +21,7 @@ jobs: - name: Set up dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.x" + dotnet-version: "9.x" - name: Test # Dependabot cannot access secrets, so we disable this step for Dependabot diff --git a/.github/workflows/website_backend_ci.yml b/.github/workflows/website_backend_ci.yml index e6a1a0b5..2859fed5 100644 --- a/.github/workflows/website_backend_ci.yml +++ b/.github/workflows/website_backend_ci.yml @@ -21,7 +21,7 @@ jobs: - name: Set up dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.x" + dotnet-version: "9.x" - name: Test # Dependabot cannot access secrets, so we disable this step for Dependabot diff --git a/.github/workflows/website_cd.yml b/.github/workflows/website_cd.yml index a528e30e..1deb2436 100644 --- a/.github/workflows/website_cd.yml +++ b/.github/workflows/website_cd.yml @@ -21,7 +21,7 @@ jobs: - name: Set up dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.x" + dotnet-version: "9.x" - name: Restore Nugets run: dotnet restore "${{ env.WORKING_DIRECTORY }}" diff --git a/.github/workflows/website_frontend_ci.yml b/.github/workflows/website_frontend_ci.yml index 37a0fabe..f60cd983 100644 --- a/.github/workflows/website_frontend_ci.yml +++ b/.github/workflows/website_frontend_ci.yml @@ -17,7 +17,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest container: - image: mcr.microsoft.com/playwright/dotnet:v1.45.1-jammy + image: mcr.microsoft.com/playwright/dotnet:v1.49.0-jammy options: --user 1001 steps: - name: Checkout @@ -26,7 +26,7 @@ jobs: - name: Set up dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.x" + dotnet-version: "9.x" - name: Test run: > diff --git a/benchmarks/Jordnaer.Benchmarks/Jordnaer.Benchmarks.csproj b/benchmarks/Jordnaer.Benchmarks/Jordnaer.Benchmarks.csproj index 5143eab5..a50ede04 100644 --- a/benchmarks/Jordnaer.Benchmarks/Jordnaer.Benchmarks.csproj +++ b/benchmarks/Jordnaer.Benchmarks/Jordnaer.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable @@ -10,8 +10,8 @@ - - + + diff --git a/src/container_apps/Jordnaer.Chat/Jordnaer.Chat.csproj b/src/container_apps/Jordnaer.Chat/Jordnaer.Chat.csproj index 9439fac9..c94e4131 100644 --- a/src/container_apps/Jordnaer.Chat/Jordnaer.Chat.csproj +++ b/src/container_apps/Jordnaer.Chat/Jordnaer.Chat.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 6af4abd1-e8ca-4aa2-a593-15395d30dc2a Linux ..\..\.. @@ -11,12 +11,12 @@ - + - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/container_apps/Jordnaer.Chat/Program.cs b/src/container_apps/Jordnaer.Chat/Program.cs index 6118692a..c865eae6 100644 --- a/src/container_apps/Jordnaer.Chat/Program.cs +++ b/src/container_apps/Jordnaer.Chat/Program.cs @@ -11,8 +11,6 @@ { var builder = WebApplication.CreateBuilder(args); - builder.AddAzureAppConfiguration(); - builder.AddSerilog(); builder.AddDatabase(); diff --git a/src/shared/Jordnaer.Shared.Infrastructure/AzureAppConfigurationExtensions.cs b/src/shared/Jordnaer.Shared.Infrastructure/AzureAppConfigurationExtensions.cs deleted file mode 100644 index 03e7f8e6..00000000 --- a/src/shared/Jordnaer.Shared.Infrastructure/AzureAppConfigurationExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.FeatureManagement; - -namespace Jordnaer.Shared.Infrastructure; - -public static class AzureAppConfigurationExtensions -{ - public static WebApplicationBuilder AddAzureAppConfiguration(this WebApplicationBuilder builder) - { - builder.Services.AddFeatureManagement(); - - if (builder.Environment.IsDevelopment()) - { - return builder; - } - - var connectionString = builder.Configuration.GetConnectionString("AppConfig") - ?? throw new InvalidOperationException( - "Failed to find connection string to Azure App Configuration. " + - "Keys checked: 'ConnectionStrings:AppConfig'"); - - builder.Configuration.AddAzureAppConfiguration(options => - options.Connect(connectionString) - // Load all keys that have no label - .Select("*") - .ConfigureRefresh(refreshOptions => - // Only reload configs if the 'Sentinel' key is modified - refreshOptions.Register("Sentinel", refreshAll: true)) - .UseFeatureFlags()); - builder.Services.AddAzureAppConfiguration(); - - return builder; - } -} \ No newline at end of file diff --git a/src/shared/Jordnaer.Shared.Infrastructure/Jordnaer.Shared.Infrastructure.csproj b/src/shared/Jordnaer.Shared.Infrastructure/Jordnaer.Shared.Infrastructure.csproj index 63cad0c6..3edfaa45 100644 --- a/src/shared/Jordnaer.Shared.Infrastructure/Jordnaer.Shared.Infrastructure.csproj +++ b/src/shared/Jordnaer.Shared.Infrastructure/Jordnaer.Shared.Infrastructure.csproj @@ -1,16 +1,15 @@  - net8.0 + net9.0 - + - - + + - - + diff --git a/src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj b/src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj index 2df7c081..43c92318 100644 --- a/src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj +++ b/src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj @@ -1,20 +1,20 @@  - net8.0 + net9.0 Jordnaer.Shared - - - - - - + + + + + + - - + + diff --git a/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor b/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor index 759636ad..27ad5f3a 100644 --- a/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor +++ b/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor @@ -15,7 +15,7 @@

Registreringsbekræftelse

Check venligst din email for at bekræfte din konto. - Husk at tjekke uønsket mail mappen. + Husk at tjekke spam/uønsket mail. @code { diff --git a/src/web/Jordnaer/Components/ScrollToTopComponentBase.cs b/src/web/Jordnaer/Components/ScrollToTopComponentBase.cs new file mode 100644 index 00000000..1fbe0105 --- /dev/null +++ b/src/web/Jordnaer/Components/ScrollToTopComponentBase.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Jordnaer.Components; + +public class ScrollToTopComponentBase : ComponentBase, IDisposable, IAsyncDisposable +{ + [Inject] + private IScrollManager ScrollManager { get; set; } = null!; + + private CancellationTokenSource? _cancellationTokenSource; + + protected CancellationToken CancellationToken => (_cancellationTokenSource ??= new CancellationTokenSource()).Token; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await ScrollManager.ScrollToTopAsync("html"); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + + if (_cancellationTokenSource is null) + { + return; + } + + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + + if (_cancellationTokenSource is null) + { + return; + } + + await _cancellationTokenSource.CancelAsync(); + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } +} \ No newline at end of file diff --git a/src/web/Jordnaer/Extensions/JsRuntimeExtensions.cs b/src/web/Jordnaer/Extensions/JsRuntimeExtensions.cs index c129f376..e55ed93e 100644 --- a/src/web/Jordnaer/Extensions/JsRuntimeExtensions.cs +++ b/src/web/Jordnaer/Extensions/JsRuntimeExtensions.cs @@ -17,6 +17,11 @@ public static async Task HideElement(this IJSRuntime jsRuntime, string sel => await jsRuntime.InvokeVoidAsyncWithErrorHandling( "utilities.hideElement", selector); + + public static async Task ShowElement(this IJSRuntime jsRuntime, string selector) + => await jsRuntime.InvokeVoidAsyncWithErrorHandling( + "utilities.showElement", + selector); public static async ValueTask GetGeolocation(this IJSRuntime jsRuntime) { var (success, geoLocation) = await jsRuntime.InvokeAsyncWithErrorHandling("utilities.getGeolocation"); diff --git a/src/web/Jordnaer/Extensions/MudColorExtensions.cs b/src/web/Jordnaer/Extensions/MudColorExtensions.cs new file mode 100644 index 00000000..9f2161cc --- /dev/null +++ b/src/web/Jordnaer/Extensions/MudColorExtensions.cs @@ -0,0 +1,10 @@ +using MudBlazor.Utilities; + +namespace Jordnaer.Extensions; + +public static class MudColorExtensions +{ + public static string ToBackgroundColor(this MudColor color) => $"background-color: {color}"; + + public static string ToTextColor(this MudColor color) => $"color: {color}"; +} diff --git a/src/web/Jordnaer/Features/Chat/ChatNotificationService.cs b/src/web/Jordnaer/Features/Chat/ChatNotificationService.cs index cba8a5ba..bc6c389e 100644 --- a/src/web/Jordnaer/Features/Chat/ChatNotificationService.cs +++ b/src/web/Jordnaer/Features/Chat/ChatNotificationService.cs @@ -11,13 +11,14 @@ namespace Jordnaer.Features.Chat; public class ChatNotificationService( - JordnaerDbContext context, + IDbContextFactory contextFactory, ILogger logger, IPublishEndpoint publishEndpoint, IServer server) { public async Task NotifyRecipients(StartChat startChat, CancellationToken cancellationToken = default) { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var recipientIds = startChat.Recipients.Select(recipient => recipient.Id); var recipients = await context.Users .AsNoTracking() diff --git a/src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs b/src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs index f43234b3..b1c91b72 100644 --- a/src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs +++ b/src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs @@ -12,7 +12,7 @@ public interface IGroupSearchService } public class GroupSearchService( - JordnaerDbContext context, + IDbContextFactory contextFactory, IZipCodeService zipCodeService) : IGroupSearchService { @@ -21,6 +21,7 @@ public async Task GetGroupsAsync(GroupSearchFilter filter, { JordnaerMetrics.GroupSearchesCounter.Add(1, MakeTagList(filter)); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var groups = context.Groups .AsNoTracking() .AsQueryable() diff --git a/src/web/Jordnaer/Features/GroupSearch/GroupSearchServiceExtensions.cs b/src/web/Jordnaer/Features/GroupSearch/QueryableGroupExtensions.cs similarity index 88% rename from src/web/Jordnaer/Features/GroupSearch/GroupSearchServiceExtensions.cs rename to src/web/Jordnaer/Features/GroupSearch/QueryableGroupExtensions.cs index e0c3fc99..75b3994c 100644 --- a/src/web/Jordnaer/Features/GroupSearch/GroupSearchServiceExtensions.cs +++ b/src/web/Jordnaer/Features/GroupSearch/QueryableGroupExtensions.cs @@ -2,7 +2,7 @@ namespace Jordnaer.Features.GroupSearch; -internal static class GroupSearchServiceExtensions +internal static class QueryableGroupExtensions { internal static IQueryable ApplyNameFilter(this IQueryable groups, string? name) { diff --git a/src/web/Jordnaer/Features/Images/ExternalProfilePictureDownloader.cs b/src/web/Jordnaer/Features/Images/ExternalProfilePictureDownloader.cs index 3b435ddb..c4ed0556 100644 --- a/src/web/Jordnaer/Features/Images/ExternalProfilePictureDownloader.cs +++ b/src/web/Jordnaer/Features/Images/ExternalProfilePictureDownloader.cs @@ -1,10 +1,11 @@ using Jordnaer.Database; using Mediator; +using Microsoft.EntityFrameworkCore; namespace Jordnaer.Features.Images; public class ExternalProfilePictureDownloader( - JordnaerDbContext context, + IDbContextFactory contextFactory, ILogger logger, IEnumerable providerPictureDownloader) : INotificationHandler @@ -19,6 +20,7 @@ public async ValueTask Handle(AccessTokenAcquired notification, CancellationToke return; } + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var user = await context.UserProfiles.FindAsync([notification.UserId], cancellationToken); if (user is null) { diff --git a/src/web/Jordnaer/Features/Images/ProfileImageService.cs b/src/web/Jordnaer/Features/Images/ProfileImageService.cs index 3471608d..b2173d8d 100644 --- a/src/web/Jordnaer/Features/Images/ProfileImageService.cs +++ b/src/web/Jordnaer/Features/Images/ProfileImageService.cs @@ -11,7 +11,7 @@ public interface IProfileImageService Task SetGroupProfilePictureAsync(SetGroupProfilePicture dto); } -public class ProfileImageService(JordnaerDbContext context, IImageService imageService) : IProfileImageService +public class ProfileImageService(IDbContextFactory contextFactory, IImageService imageService) : IProfileImageService { public const string ChildProfilePicturesContainerName = "childprofile-pictures"; public const string UserProfilePicturesContainerName = "userprofile-pictures"; @@ -50,14 +50,18 @@ public async Task SetGroupProfilePictureAsync(SetGroupProfilePicture dto return uri; } - private async Task UpdateChildProfilePictureAsync(SetChildProfilePicture dto, string uri) + private async Task UpdateChildProfilePictureAsync( + SetChildProfilePicture dto, + string uri, + CancellationToken cancellationToken = default) { - var currentChildProfile = await context.ChildProfiles.FindAsync(dto.ChildProfile.Id); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var currentChildProfile = await context.ChildProfiles.FindAsync([dto.ChildProfile.Id], cancellationToken); if (currentChildProfile is null) { dto.ChildProfile.PictureUrl = uri; context.ChildProfiles.Add(dto.ChildProfile); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); return; } @@ -67,18 +71,22 @@ private async Task UpdateChildProfilePictureAsync(SetChildProfilePicture dto, st currentChildProfile.PictureUrl = uri; context.Entry(currentChildProfile).State = EntityState.Modified; - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); } } - private async Task UpdateUserProfilePictureAsync(SetUserProfilePicture dto, string uri) + private async Task UpdateUserProfilePictureAsync( + SetUserProfilePicture dto, + string uri, + CancellationToken cancellationToken = default) { - var currentUserProfile = await context.UserProfiles.FindAsync(dto.UserProfile.Id); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var currentUserProfile = await context.UserProfiles.FindAsync([dto.UserProfile.Id], cancellationToken); if (currentUserProfile is null) { dto.UserProfile.ProfilePictureUrl = uri; context.UserProfiles.Add(dto.UserProfile); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); return; } @@ -88,17 +96,21 @@ private async Task UpdateUserProfilePictureAsync(SetUserProfilePicture dto, stri currentUserProfile.ProfilePictureUrl = uri; context.Entry(currentUserProfile).State = EntityState.Modified; - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); } } - private async Task UpdateGroupProfilePictureAsync(SetGroupProfilePicture dto, string uri) + private async Task UpdateGroupProfilePictureAsync( + SetGroupProfilePicture dto, + string uri, + CancellationToken cancellationToken = default) { - var currentGroup = await context.Groups.FindAsync(dto.Group.Id); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var currentGroup = await context.Groups.FindAsync([dto.Group.Id], cancellationToken); if (currentGroup is null) { dto.Group.ProfilePictureUrl = uri; context.Groups.Add(dto.Group); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); return; } @@ -108,7 +120,7 @@ private async Task UpdateGroupProfilePictureAsync(SetGroupProfilePicture dto, st currentGroup.ProfilePictureUrl = uri; context.Entry(currentGroup).State = EntityState.Modified; - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/web/Jordnaer/Features/Theme/Bees.razor b/src/web/Jordnaer/Features/Theme/Bees.razor new file mode 100644 index 00000000..cf5297ca --- /dev/null +++ b/src/web/Jordnaer/Features/Theme/Bees.razor @@ -0,0 +1,22 @@ +@if (Center) +{ +
+ +
+} +else +{ + +} + +@code { + [Parameter] + public string? Class { get; set; } + [Parameter] + public string? Style { get; set; } + [Parameter] + public bool Center { get; set; } = false; + + private int _width = 150; + private static readonly string _src = "/images/bier.png"; +} diff --git a/src/web/Jordnaer/Features/Theme/JordnaerPalette.cs b/src/web/Jordnaer/Features/Theme/JordnaerPalette.cs index a635ffa5..c721cd0e 100644 --- a/src/web/Jordnaer/Features/Theme/JordnaerPalette.cs +++ b/src/web/Jordnaer/Features/Theme/JordnaerPalette.cs @@ -33,7 +33,7 @@ public static class JordnaerPalette /// /// Yellow-orange. Used as background for texts and headers /// - public static readonly MudColor YellowBackground = "#dbab45"; + public static readonly MudColor YellowBackground = "#fcca3f"; /// /// Green. Used as background for texts and headers @@ -43,28 +43,20 @@ public static class JordnaerPalette /// /// Dark Blue. Used for body text /// - public static readonly MudColor BlueBody = "#41556b"; - - public static readonly string BlueBodyTextStyle = $"color: {BlueBody}"; + public static readonly MudColor BlueBody = "#41556b"; /// /// Dark Red. Used for small texts, payoffs, quotes /// - public static readonly MudColor RedHeader = "#673417"; - - public static readonly string RedHeaderTextStyle = $"color: {RedHeader}"; + public static readonly MudColor RedHeader = "#673417"; /// /// Beige. Used as background for text where and are too dark/saturated. /// public static readonly MudColor BeigeBackground = "#cfc1a699"; // 99 added to apply 60% opacity - public static readonly string BeigeBackgroundStyle = $"background-color: {BeigeBackground}"; - /// /// Pale Blue. Rarely used as background for text where and are too dark/saturated. /// public static readonly MudColor PaleBlueBackground = "#a9c0cf66"; // 66 added to apply 40% opacity - - public static readonly string PaleBlueBackgroundStyle = $"background-color: {PaleBlueBackground}"; } diff --git a/src/web/Jordnaer/Features/Theme/MiniDivider.razor b/src/web/Jordnaer/Features/Theme/MiniDivider.razor index cb509da9..d45121cc 100644 --- a/src/web/Jordnaer/Features/Theme/MiniDivider.razor +++ b/src/web/Jordnaer/Features/Theme/MiniDivider.razor @@ -16,7 +16,6 @@ else public string? Style { get; set; } [Parameter] public required MiniDividerColor Color { get; set; } - [Parameter] public bool Center { get; set; } = false; diff --git a/src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs b/src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs new file mode 100644 index 00000000..f0806626 --- /dev/null +++ b/src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs @@ -0,0 +1,93 @@ +using Jordnaer.Features.Search; +using Jordnaer.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Jordnaer.Features.UserSearch; + +internal static class QueryableUserProfileExtensions +{ + internal static async Task<(IQueryable UserProfiles, bool AppliedOrdering)> ApplyLocationFilterAsync( + this IQueryable users, + UserSearchFilter filter, + IZipCodeService zipCodeService, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(filter.Location) || filter.WithinRadiusKilometers is null) + { + return (users, false); + } + + var (zipCodesWithinCircle, searchedZipCode) = await zipCodeService.GetZipCodesNearLocationAsync( + filter.Location, + filter.WithinRadiusKilometers.Value, + cancellationToken); + + if (zipCodesWithinCircle.Count is 0 || searchedZipCode is null) + { + return (users, false); + } + + users = users.Where(user => user.ZipCode != null && + zipCodesWithinCircle.Contains(user.ZipCode.Value)) + .OrderBy(user => Math.Abs(user.ZipCode!.Value - searchedZipCode.Value)); + + return (users, true); + } + + internal static IQueryable ApplyCategoryFilter(this IQueryable users, + UserSearchFilter filter) + { + if (filter.Categories is not null && filter.Categories.Length > 0) + { + users = users.Where(user => + user.Categories.Any(category => filter.Categories.Contains(category.Name))); + } + + return users; + } + + internal static IQueryable ApplyNameFilter(this IQueryable users, string? filter) + { + if (!string.IsNullOrWhiteSpace(filter)) + { + users = users.Where(user => !string.IsNullOrEmpty(user.SearchableName) && + EF.Functions.Like(user.SearchableName, $"%{filter}%")); + } + + return users; + } + + internal static IQueryable ApplyChildFilters(this IQueryable users,UserSearchFilter filter) + { + if (filter.ChildGender is not null) + { + users = users.Where(user => + user.ChildProfiles.Any(child => child.Gender == filter.ChildGender)); + } + + if (filter is { MinimumChildAge: not null, MaximumChildAge: not null } && + filter.MinimumChildAge == filter.MaximumChildAge) + { + users = users.Where(user => + user.ChildProfiles.Any(child => child.Age != null && + child.Age == filter.MinimumChildAge)); + return users; + } + + if (filter.MinimumChildAge is not null) + { + users = users.Where(user => + user.ChildProfiles.Any(child => child.Age != null && + child.Age >= filter.MinimumChildAge)); + } + + if (filter.MaximumChildAge is not null) + { + users = users.Where(user => + user.ChildProfiles.Any(child => child.Age != null && + child.Age <= filter.MaximumChildAge)); + } + + return users; + } +} diff --git a/src/web/Jordnaer/Features/UserSearch/UserSearchService.cs b/src/web/Jordnaer/Features/UserSearch/UserSearchService.cs index 50ecc808..1dcbba7a 100644 --- a/src/web/Jordnaer/Features/UserSearch/UserSearchService.cs +++ b/src/web/Jordnaer/Features/UserSearch/UserSearchService.cs @@ -1,4 +1,5 @@ using Jordnaer.Database; +using Jordnaer.Features.GroupSearch; using Jordnaer.Features.Metrics; using Jordnaer.Features.Search; using Jordnaer.Shared; @@ -14,18 +15,18 @@ public interface IUserSearchService public class UserSearchService( IZipCodeService zipCodeService, - JordnaerDbContext context) + IDbContextFactory contextFactory) : IUserSearchService { - public async Task> GetUsersByNameAsync(string currentUserId, string searchString, CancellationToken cancellationToken = default) { - var users = ApplyNameFilter(searchString, context.UserProfiles); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var users = context.UserProfiles.ApplyNameFilter(searchString); var firstTenUsers = await users .Where(user => user.Id != currentUserId) .OrderBy(user => searchString.StartsWith(searchString)) - .Take(11) // We take 11 to see if there are more than 10 users (to show a "more" button) + .Take(11) // We take 11 to let the frontend know we might have more than it searched for .Select(user => new UserSlim { ProfilePictureUrl = user.ProfilePictureUrl, @@ -43,14 +44,16 @@ public async Task GetUsersAsync(UserSearchFilter filter, Cance { JordnaerMetrics.UserSearchesCounter.Add(1, MakeTagList(filter)); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var users = context.UserProfiles .Where(user => !string.IsNullOrEmpty(user.UserName)) .AsQueryable(); - users = ApplyChildFilters(filter, users); - users = ApplyNameFilter(filter.Name, users); - users = ApplyCategoryFilter(filter, users); - (users, var isOrdered) = await ApplyLocationFilterAsync(filter, users, cancellationToken); + users = users.ApplyChildFilters(filter); + users = users.ApplyNameFilter(filter.Name); + users = users.ApplyCategoryFilter(filter); + (users, var isOrdered) = await users.ApplyLocationFilterAsync(filter, zipCodeService, cancellationToken); if (!isOrdered) { @@ -92,90 +95,6 @@ public async Task GetUsersAsync(UserSearchFilter filter, Cance return new UserSearchResult { TotalCount = totalCount, Users = paginatedUsers }; } - internal async Task<(IQueryable UserProfiles, bool AppliedOrdering)> ApplyLocationFilterAsync( - UserSearchFilter filter, - IQueryable users, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(filter.Location) || filter.WithinRadiusKilometers is null) - { - return (users, false); - } - - var (zipCodesWithinCircle, searchedZipCode) = await zipCodeService.GetZipCodesNearLocationAsync( - filter.Location, - filter.WithinRadiusKilometers.Value, - cancellationToken); - - if (zipCodesWithinCircle.Count is 0 || searchedZipCode is null) - { - return (users, false); - } - - // TODO: This ordering should be done after the "client" has received it, to enable caching - users = users.Where(user => user.ZipCode != null && - zipCodesWithinCircle.Contains(user.ZipCode.Value)) - .OrderBy(user => Math.Abs(user.ZipCode!.Value - searchedZipCode.Value)); - - return (users, true); - } - - internal static IQueryable ApplyCategoryFilter(UserSearchFilter filter, IQueryable users) - { - if (filter.Categories is not null && filter.Categories.Length > 0) - { - users = users.Where(user => - user.Categories.Any(category => filter.Categories.Contains(category.Name))); - } - - return users; - } - - internal static IQueryable ApplyNameFilter(string? filter, IQueryable users) - { - if (!string.IsNullOrWhiteSpace(filter)) - { - users = users.Where(user => !string.IsNullOrEmpty(user.SearchableName) && - EF.Functions.Like(user.SearchableName, $"%{filter}%")); - } - - return users; - } - - internal static IQueryable ApplyChildFilters(UserSearchFilter filter, IQueryable users) - { - if (filter.ChildGender is not null) - { - users = users.Where(user => - user.ChildProfiles.Any(child => child.Gender == filter.ChildGender)); - } - - if (filter is { MinimumChildAge: not null, MaximumChildAge: not null } && - filter.MinimumChildAge == filter.MaximumChildAge) - { - users = users.Where(user => - user.ChildProfiles.Any(child => child.Age != null && - child.Age == filter.MinimumChildAge)); - return users; - } - - if (filter.MinimumChildAge is not null) - { - users = users.Where(user => - user.ChildProfiles.Any(child => child.Age != null && - child.Age >= filter.MinimumChildAge)); - } - - if (filter.MaximumChildAge is not null) - { - users = users.Where(user => - user.ChildProfiles.Any(child => child.Age != null && - child.Age <= filter.MaximumChildAge)); - } - - return users; - } - private static ReadOnlySpan> MakeTagList(UserSearchFilter filter) { return new KeyValuePair[] diff --git a/src/web/Jordnaer/Jordnaer.csproj b/src/web/Jordnaer/Jordnaer.csproj index 40269485..c1d21b81 100644 --- a/src/web/Jordnaer/Jordnaer.csproj +++ b/src/web/Jordnaer/Jordnaer.csproj @@ -1,35 +1,35 @@  - net8.0 + net9.0 d330f5dc-6b2c-408b-bc04-d3b0ff28178b preview - + - + - + - - - - - - - - - - + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + @@ -37,13 +37,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + diff --git a/src/web/Jordnaer/Pages/Chat/ChatPage.razor b/src/web/Jordnaer/Pages/Chat/ChatPage.razor index 6731c6b2..18dc35d0 100644 --- a/src/web/Jordnaer/Pages/Chat/ChatPage.razor +++ b/src/web/Jordnaer/Pages/Chat/ChatPage.razor @@ -281,5 +281,6 @@ { await BrowserViewportService.UnsubscribeAsync(_breakpointObserverId); await ChatSignalRClient.StopAsync(); + await JsRuntime.ShowElement(".footer"); } } diff --git a/src/web/Jordnaer/Pages/Footer/About.razor b/src/web/Jordnaer/Pages/Footer/About.razor index e7894f5b..6404999c 100644 --- a/src/web/Jordnaer/Pages/Footer/About.razor +++ b/src/web/Jordnaer/Pages/Footer/About.razor @@ -1,56 +1,59 @@ @page "/about" +@inherits ScrollToTopComponentBase + - + -

Hvad er Mini Møder?

+

Hvad er

+ Mini Møder Logo - Mini Møder er en hjemmeside til os, der ønsker at skabe nye relationer med andre børnefamilier. + Mini Møder er en hjemmeside til os, der ønsker at skabe nye relationer med andre børnefamilier. - Siden er skabt med særligt henblik på at hjælpe legegrupper, - hjemmegrupper og hjemmeskoler med at blive dannet og gro. - + Siden er skabt med særligt henblik på at hjælpe legegrupper, + hjemmegrupper og hjemmeskoler med at blive dannet og gro. + - Vi vil gøre det nemmere for forældre at finde venner til deres børn. - + Vi vil gøre det nemmere for børn at finde nye venner. + - + -

- Hvorfor har vi skabt Mini Møder? -

+

+ Hvorfor har vi skabt Mini Møder? +

- Vi har lavet Mini Møder fordi vi selv har manglet en konkret platform til at lede - efter legegrupper til vores børn. + Vi har lavet Mini Møder fordi vi selv har manglet en konkret platform til at lede + efter legegrupper til vores børn. - Da vi hjemmepassede vores børn, fandt vi det utrolig besværligt og kompliceret at - finde en fast gruppe af børn og voksne, - som vi kunne ses med jævnligt og skabe minder sammen med. - + Da vi hjemmepassede vores børn, fandt vi det utrolig besværligt og kompliceret at + finde en fast gruppe af børn og voksne, + som vi kunne ses med jævnligt og skabe minder sammen med. + - Vi har et dybt ønske om, at Mini Møder kan blive en grobund for tætte relationer, - gode venner og stærke forbindelser.Tryghed og tillid er efter vores mening afgørende - for børns velbefindende og trivsel. - + Vi har et dybt ønske om, at Mini Møder kan blive en grobund for tætte relationer, + gode venner og stærke forbindelser.Tryghed og tillid er efter vores mening afgørende + for børns velbefindende og trivsel. + - Børnenes primære omsorgspersoner, tætte relationer og stærke fællesskaber kan støtte - barnets sundhed og trivsel, og Mini Møder kan hjælpe med at finde disse fællesskaber, - hvor børnenes præmisser er i højsædet. - + Børnenes primære omsorgspersoner, tætte relationer og stærke fællesskaber kan støtte + barnets sundhed og trivsel, og Mini Møder kan hjælpe med at finde disse fællesskaber, + hvor børnenes præmisser er i højsædet. + - + -
-
+ + \ No newline at end of file diff --git a/src/web/Jordnaer/Pages/Footer/Contact.razor b/src/web/Jordnaer/Pages/Footer/Contact.razor index 3f47f672..e5a863a7 100644 --- a/src/web/Jordnaer/Pages/Footer/Contact.razor +++ b/src/web/Jordnaer/Pages/Footer/Contact.razor @@ -2,14 +2,16 @@ @inject IEmailService EmailService @inject ISnackbar Snackbar +@inherits ScrollToTopComponentBase + - + -

Kontakt os

+

Kontakt os

@@ -45,7 +47,7 @@ AutoGrow Class="my-3"/> - - + -

+

Drift

@@ -18,7 +20,7 @@ Jeg bygger Mini Møder i min fritid, oftest om aftenen når mine to unger er puttet. Jeg står for udvikling, drift og vedligeholdelse af hele molevitten. - + @@ -27,7 +29,7 @@ Du kan komme i kontakt med os gennem vores kontaktformular. - + Af juridiske årsager ejer jeg Mini Møder gennem min enkeltmandsvirksomhed:
diff --git a/src/web/Jordnaer/Pages/Footer/Privacy.razor b/src/web/Jordnaer/Pages/Footer/Privacy.razor index 9882819d..122b2545 100644 --- a/src/web/Jordnaer/Pages/Footer/Privacy.razor +++ b/src/web/Jordnaer/Pages/Footer/Privacy.razor @@ -1,12 +1,14 @@ @page "/privacy" +@inherits ScrollToTopComponentBase + - + -

+

Privatlivspolitik

diff --git a/src/web/Jordnaer/Pages/Footer/Sponsors.razor b/src/web/Jordnaer/Pages/Footer/Sponsors.razor index 50632855..e3df0ce3 100644 --- a/src/web/Jordnaer/Pages/Footer/Sponsors.razor +++ b/src/web/Jordnaer/Pages/Footer/Sponsors.razor @@ -1,59 +1,62 @@ @page "/sponsors" +@inherits ScrollToTopComponentBase + - + -

- Vore Sponsorer -

+

+ Vore Sponsorer +

- + - - @foreach (var sponsor in _sponsors) - { - - } - + + @foreach (var sponsor in _sponsors) + { + + } + - + -

- Bliv Sponsor -

+

+ Bliv Sponsor +

- - At være en sponsor af Mini Møder giver en unik mulighed for at støtte et - lokalt initiativ og samtidig opnå synlighed for dit brand. + + At være sponsor af Mini Møder giver en unik mulighed for at støtte vores arbejde + og samtidig opnå synlighed for dit brand. - Hvis du er interesseret i at høre mere om mulighederne for at blive en sponsor, - er du meget velkommen til at kontakte os gennem vores kontaktformular. - + Hvis du er interesseret i at høre mere om mulighederne for at blive sponsor, + er du meget velkommen til at kontakte os gennem vores kontaktformular. + -
+ +
@code { private static readonly List _sponsors = [ - new Sponsor - { - Name = string.Empty, - Description = "Moon Creative laver illustrationer og grafisk design til iværksættere og virksomheder, der ønsker at skabe forandring i mennesker", - LogoUrl = "https://usercontent.one/wp/www.mooncreative.dk/wp-content/uploads/2022/04/Logo_mooncreative_long-2-e1649063478985.png", - Link = "https://www.mooncreative.dk/" - }, - new Sponsor - { - Name = string.Empty, - Description = "Microsoft for Startups samler mennesker, viden og teknologi for at hjælpe iværksættere på alle stadier med at løse udfordringer i startfasen.", - LogoUrl = "https://i.ibb.co/v4Q7pkQ/startups-wordmark-purple.png", - Link = "https://www.microsoft.com/en-us/startups" + new Sponsor + { + Name = string.Empty, + Description = "Moon Creative laver illustrationer og grafisk design til iværksættere og virksomheder, der ønsker at skabe forandring i mennesker", + LogoUrl = "https://usercontent.one/wp/www.mooncreative.dk/wp-content/uploads/2022/04/Logo_mooncreative_long-2-e1649063478985.png", + Link = "https://www.mooncreative.dk/" + }, + new Sponsor + { + Name = string.Empty, + Description = "Microsoft for Startups samler mennesker, viden og teknologi for at hjælpe iværksættere på alle stadier med at løse udfordringer i startfasen.", + LogoUrl = "https://i.ibb.co/v4Q7pkQ/startups-wordmark-purple.png", + Link = "https://www.microsoft.com/en-us/startups" }, new Sponsor { @@ -63,5 +66,5 @@ Link = "https://elmah.io/" } ]; - + } diff --git a/src/web/Jordnaer/Pages/Footer/Terms.razor b/src/web/Jordnaer/Pages/Footer/Terms.razor index 82d329b4..fe758003 100644 --- a/src/web/Jordnaer/Pages/Footer/Terms.razor +++ b/src/web/Jordnaer/Pages/Footer/Terms.razor @@ -1,12 +1,14 @@ @page "/terms" +@inherits ScrollToTopComponentBase + - + -

+

Servicevilkår

diff --git a/src/web/Jordnaer/Pages/Home/DesktopLandingPage.razor b/src/web/Jordnaer/Pages/Home/DesktopLandingPage.razor index de8ae493..3c330840 100644 --- a/src/web/Jordnaer/Pages/Home/DesktopLandingPage.razor +++ b/src/web/Jordnaer/Pages/Home/DesktopLandingPage.razor @@ -5,7 +5,7 @@ - + OPSLAG @@ -14,13 +14,13 @@ - + - Velkommen - Til mødestedet for børnefamilier + Velkommen + Til mødestedet for børnefamilier @@ -30,7 +30,7 @@
- + Hvad kan jeg bruge Mini Møder til? diff --git a/src/web/Jordnaer/Pages/Home/LandingPageLinkComponent.razor b/src/web/Jordnaer/Pages/Home/LandingPageLinkComponent.razor index a266b92f..5d9a2e2c 100644 --- a/src/web/Jordnaer/Pages/Home/LandingPageLinkComponent.razor +++ b/src/web/Jordnaer/Pages/Home/LandingPageLinkComponent.razor @@ -7,7 +7,7 @@
- Vær' med! + Vær' med!
@code { @@ -15,6 +15,6 @@ public bool IsMobile { get; set; } private string _buttonClass => IsMobile ? "mb-5 mx-2" : "mb-5 mx-10"; - private Variant _variant = Variant.Filled; - private Color _color = Color.Default; + private Variant _variant = Variant.Outlined; + private Color _color = Color.Tertiary; } \ No newline at end of file diff --git a/src/web/Jordnaer/Pages/Home/MobileLandingPage.razor b/src/web/Jordnaer/Pages/Home/MobileLandingPage.razor index 905ff443..af2759dd 100644 --- a/src/web/Jordnaer/Pages/Home/MobileLandingPage.razor +++ b/src/web/Jordnaer/Pages/Home/MobileLandingPage.razor @@ -5,7 +5,7 @@
- + OPSLAG @@ -15,14 +15,14 @@
- + - + Velkommen - + Til mødestedet for børnefamilier @@ -34,11 +34,11 @@
- - Hvad kan jeg bruge Mini Møder til? + + Hvad kan jeg bruge Mini Møder til? - +
diff --git a/src/web/Jordnaer/Pages/Registration/FirstLogin.razor b/src/web/Jordnaer/Pages/Registration/FirstLogin.razor index 65ec16aa..f3750058 100644 --- a/src/web/Jordnaer/Pages/Registration/FirstLogin.razor +++ b/src/web/Jordnaer/Pages/Registration/FirstLogin.razor @@ -20,10 +20,10 @@ - @title + @title - @body + @body + - net8.0 + net9.0 false true @@ -12,23 +12,23 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/web/Jordnaer.E2E.Tests/Jordnaer.E2E.Tests.csproj b/tests/web/Jordnaer.E2E.Tests/Jordnaer.E2E.Tests.csproj index efcbd1c4..747339eb 100644 --- a/tests/web/Jordnaer.E2E.Tests/Jordnaer.E2E.Tests.csproj +++ b/tests/web/Jordnaer.E2E.Tests/Jordnaer.E2E.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 false true @@ -15,20 +15,20 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/web/Jordnaer.Load.Tests/Jordnaer.Load.Tests.csproj b/tests/web/Jordnaer.Load.Tests/Jordnaer.Load.Tests.csproj index 7ffc532d..40c2231b 100644 --- a/tests/web/Jordnaer.Load.Tests/Jordnaer.Load.Tests.csproj +++ b/tests/web/Jordnaer.Load.Tests/Jordnaer.Load.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 Exe false @@ -12,10 +12,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs b/tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs index 6573d240..adf9e3dc 100644 --- a/tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs +++ b/tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Moq.EntityFrameworkCore; using NSubstitute; @@ -20,7 +20,6 @@ namespace Jordnaer.Tests.Chat; public class ChatNotificationServiceTests { private readonly Mock _contextMock; - private readonly Mock> _loggerMock; private readonly Mock _publishEndpointMock; private readonly IServer _serverMock; private readonly ChatNotificationService _service; @@ -29,13 +28,18 @@ public ChatNotificationServiceTests() { _contextMock = new Mock( new DbContextOptionsBuilder().Options); - _loggerMock = new Mock>(); + + var contextFactoryMock = new Mock>(); + contextFactoryMock + .Setup(x => x.CreateDbContextAsync(It.IsAny())) + .ReturnsAsync(_contextMock.Object); + _publishEndpointMock = new Mock(); _serverMock = Substitute.For(); _service = new ChatNotificationService( - _contextMock.Object, - _loggerMock.Object, + contextFactoryMock.Object, + new NullLogger(), _publishEndpointMock.Object, _serverMock ); @@ -101,8 +105,7 @@ public async Task NotifyRecipients_ShouldFetchRecipientsAndPublishEmails() // Assert _publishEndpointMock - .Verify(p => p.Publish(It.IsAny(), -It.IsAny()), + .Verify(p => p.Publish(It.IsAny(), It.IsAny()), Times.Exactly(users.Count - 1)); // chat participants excluding the initiator } @@ -117,19 +120,19 @@ public void CreateEmails_ShouldGenerateCorrectEmailContent() Recipients = [ new UserSlim - { - Id = "initiator-id", - DisplayName = "Initiator", - ProfilePictureUrl = null, - UserName = null - }, - new UserSlim - { - Id = "recipient-id", - DisplayName = "Recipient", - ProfilePictureUrl = null, - UserName = null - } + { + Id = "initiator-id", + DisplayName = "Initiator", + ProfilePictureUrl = null, + UserName = null + }, + new UserSlim + { + Id = "recipient-id", + DisplayName = "Recipient", + ProfilePictureUrl = null, + UserName = null + } ] }; diff --git a/tests/web/Jordnaer.Tests/Groups/GroupSearchServiceExtensionsTests.cs b/tests/web/Jordnaer.Tests/Groups/GroupSearchServiceExtensionsTests.cs index 56a3f135..974e3d4f 100644 --- a/tests/web/Jordnaer.Tests/Groups/GroupSearchServiceExtensionsTests.cs +++ b/tests/web/Jordnaer.Tests/Groups/GroupSearchServiceExtensionsTests.cs @@ -6,7 +6,7 @@ namespace Jordnaer.Tests.Groups; [Trait("Category", "UnitTest")] -public class GroupSearchServiceExtensionsTests +public class QueryableGroupExtensionsTests { [Fact] public void ApplyNameFilter_WithNullName_ReturnsOriginalGroups() diff --git a/tests/web/Jordnaer.Tests/Infrastructure/JordnaerWebApplicationFactory.cs b/tests/web/Jordnaer.Tests/Infrastructure/JordnaerWebApplicationFactory.cs index c738e13b..ab13fe71 100644 --- a/tests/web/Jordnaer.Tests/Infrastructure/JordnaerWebApplicationFactory.cs +++ b/tests/web/Jordnaer.Tests/Infrastructure/JordnaerWebApplicationFactory.cs @@ -14,8 +14,9 @@ namespace Jordnaer.Tests.Infrastructure; public class JordnaerWebApplicationFactory : WebApplicationFactory, IAsyncLifetime { private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder() - .WithName($"SqlServerTestcontainer-{Guid.NewGuid()}") - .Build(); + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") // We set a specific image to circumvent this bug: https://github.com/testcontainers/testcontainers-dotnet/issues/1271 + .WithName($"SqlServerTestcontainer-{Guid.NewGuid()}") + .Build(); private readonly AzuriteContainer _azureBlobStorageContainer = new AzuriteBuilder() .WithName($"AzuriteTestcontainer-{Guid.NewGuid()}") diff --git a/tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs b/tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs index 363004bb..d5f002a8 100644 --- a/tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs +++ b/tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs @@ -7,6 +7,7 @@ namespace Jordnaer.Tests.Infrastructure; public class SqlServerContainer : IAsyncLifetime where TDbContext : DbContext { public readonly MsSqlContainer Container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") // We set a specific image to circumvent this bug: https://github.com/testcontainers/testcontainers-dotnet/issues/1271 .WithName($"SqlServerTestcontainer-{Guid.NewGuid()}") .Build(); @@ -25,6 +26,7 @@ public virtual async Task InitializeAsync() _connectionString = Container.GetConnectionString(); await using var context = CreateContext(); + await context.Database.EnsureCreatedAsync(); } diff --git a/tests/web/Jordnaer.Tests/Jordnaer.Tests.csproj b/tests/web/Jordnaer.Tests/Jordnaer.Tests.csproj index 421d2232..30578637 100644 --- a/tests/web/Jordnaer.Tests/Jordnaer.Tests.csproj +++ b/tests/web/Jordnaer.Tests/Jordnaer.Tests.csproj @@ -1,33 +1,33 @@  - net8.0 + net9.0 false true - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs b/tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs index 86803e52..091bfecc 100644 --- a/tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs +++ b/tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs @@ -8,24 +8,27 @@ using Microsoft.EntityFrameworkCore; using NSubstitute; using Jordnaer.Features.Search; +using Moq; using Xunit; namespace Jordnaer.Tests.UserSearch; [Trait("Category", "IntegrationTest")] [Collection(nameof(SqlServerContainerCollection))] -public class UserSearchServiceTests : IAsyncLifetime +public class UserSearchServiceTests { - private readonly JordnaerDbContext _context; + private readonly IDbContextFactory _contextFactory = Substitute.For>(); private readonly IZipCodeService _zipCodeServiceMock = Substitute.For(); private readonly UserSearchService _sut; private readonly Faker _faker = new(); + private readonly JordnaerDbContext _context; public UserSearchServiceTests(SqlServerContainer sqlServerContainer) { _context = sqlServerContainer.CreateContext(); + _contextFactory.CreateDbContextAsync().ReturnsForAnyArgs(_context); - _sut = new UserSearchService(_zipCodeServiceMock, _context); + _sut = new UserSearchService(_zipCodeServiceMock, _contextFactory); } [Fact] @@ -35,10 +38,10 @@ public async Task Return_UserSearchResult_Given_Valid_Filter() var filter = new UserSearchFilter(); // Act + await _context.UserProfiles.ExecuteDeleteAsync(); var result = await _sut.GetUsersAsync(filter); // Assert - result.Should().BeOfType(); result.TotalCount.Should().Be(0); result.Users.Should().BeEquivalentTo(new List()); } @@ -91,6 +94,7 @@ public async Task Return_UserSearchResult_With_LastName_Filter() var users = CreateTestUsers(5); // Ensure at least one user has the specified name in their SearchableName users[0].LastName = lastName; + _context.UserProfiles.RemoveRange(_context.UserProfiles); _context.UserProfiles.AddRange(users); await _context.SaveChangesAsync(); @@ -159,6 +163,7 @@ public async Task Return_UserSearchResult_With_ChildGender_Filter() Gender = filter.ChildGender.Value, FirstName = _faker.Name.FirstName() }); + _context.UserProfiles.RemoveRange(_context.UserProfiles); _context.UserProfiles.AddRange(users); await _context.SaveChangesAsync(); @@ -182,6 +187,7 @@ public async Task Return_UserSearchResult_With_MinimumChildAge_Filter() DateOfBirth = DateTime.UtcNow.AddYears(-filter.MinimumChildAge.Value), FirstName = _faker.Name.FirstName() }); + _context.UserProfiles.RemoveRange(_context.UserProfiles); _context.UserProfiles.AddRange(users); await _context.SaveChangesAsync(); @@ -261,8 +267,4 @@ private static List CreateTestUsers(int count) return users; } - - public Task InitializeAsync() => Task.CompletedTask; - - public async Task DisposeAsync() => await _context.UserProfiles.ExecuteDeleteAsync(); }