feat(discovery): schedule saved-search alerts
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user