Files
jobtrackingapp/JobTrackerApi.Tests/SavedJobSearchAlertTests.cs
T
2026-08-31 22:23:34 +02:00

99 lines
4.5 KiB
C#

using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class SavedJobSearchAlertTests
{
[Fact]
public async Task Worker_establishes_baseline_then_notifies_only_for_later_results()
{
var clock = new MutableTimeProvider(new DateTimeOffset(2026, 8, 31, 8, 0, 0, TimeSpan.Zero));
var discovery = new FakeDiscovery(clock, "job-1");
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Workers:SavedJobSearchAlertsEnabled"] = "true",
["JobDiscovery:SavedSearchIntervalHours"] = "6"
}).Build();
var services = new ServiceCollection();
var databaseName = Guid.NewGuid().ToString();
services.AddLogging();
services.AddHttpContextAccessor();
services.AddSingleton<IConfiguration>(configuration);
services.AddSingleton<TimeProvider>(clock);
services.AddSingleton<IJobDiscoverySearchService>(discovery);
services.AddScoped<CurrentUserService>();
services.AddScoped<ICurrentUserService>(provider => provider.GetRequiredService<CurrentUserService>());
services.AddDbContext<JobTrackerContext>((_, options) => options.UseInMemoryDatabase(databaseName));
services.AddScoped<SavedJobSearchRunner>();
services.AddSingleton<BackgroundTenantRunner>();
await using var provider = services.BuildServiceProvider();
await using (var scope = provider.CreateAsyncScope())
{
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
db.SavedJobSearches.Add(new SavedJobSearch
{
OwnerUserId = "owner-without-jobs",
Name = "Backend Oslo",
Query = "backend",
Location = "Oslo",
CreatedAtUtc = clock.GetUtcNow(),
UpdatedAtUtc = clock.GetUtcNow()
});
await db.SaveChangesAsync();
}
var worker = new SavedJobSearchAlertHostedService(
provider.GetRequiredService<BackgroundTenantRunner>(),
configuration,
NullLogger<SavedJobSearchAlertHostedService>.Instance,
Mock.Of<IStartupReadiness>(),
clock);
Assert.Equal(new BackgroundWorkerRunResult(true, 1, 1, 0), await worker.RunOnceAsync(default));
await using (var scope = provider.CreateAsyncScope())
{
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
Assert.Empty(await db.UserNotifications.IgnoreQueryFilters().ToListAsync());
Assert.Single(await db.SavedJobSearchResults.IgnoreQueryFilters().ToListAsync());
}
clock.SetUtcNow(clock.GetUtcNow().AddHours(7));
discovery.Ids.Add("job-2");
Assert.Equal(new BackgroundWorkerRunResult(true, 1, 1, 0), await worker.RunOnceAsync(default));
await using var verification = provider.CreateAsyncScope();
var notification = Assert.Single(await verification.ServiceProvider.GetRequiredService<JobTrackerContext>()
.UserNotifications.IgnoreQueryFilters().AsNoTracking().ToListAsync());
Assert.Equal("owner-without-jobs", notification.OwnerUserId);
Assert.Equal("saved_search_results", notification.Kind);
Assert.Equal("/discover", notification.LinkPath);
Assert.Contains("Backend Oslo", notification.Message);
}
private sealed class FakeDiscovery(MutableTimeProvider clock, params string[] ids) : IJobDiscoverySearchService
{
public List<string> Ids { get; } = [.. ids];
public Task<IReadOnlyList<DiscoveredJob>> SearchAsync(string? query, string? location, CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<DiscoveredJob>>(Ids.Select(id => new DiscoveredJob(
id, "Backend Developer", "Acme", "Oslo", clock.GetUtcNow(), null,
$"https://example.test/{id}", "nav", "NAV Arbeidsplassen", "searched", clock.GetUtcNow(), "NO")).ToList());
}
private sealed class MutableTimeProvider(DateTimeOffset now) : TimeProvider
{
private DateTimeOffset _now = now;
public override DateTimeOffset GetUtcNow() => _now;
public void SetUtcNow(DateTimeOffset value) => _now = value;
}
}