108 lines
6.0 KiB
C#
108 lines
6.0 KiB
C#
using System.Security.Claims;
|
|
using JobTrackerApi.Controllers;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Tests.TestSupport;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
// Real SQLite: EF InMemory cannot prove LIKE, relational ordering or tenant joins.
|
|
public sealed class JobQueryParityTests
|
|
{
|
|
[Theory]
|
|
[InlineData("all")]
|
|
[InlineData("search")]
|
|
[InlineData("notes")]
|
|
[InlineData("correspondence")]
|
|
[InlineData("standard")]
|
|
[InlineData("custom")]
|
|
[InlineData("combined")]
|
|
[InlineData("none")]
|
|
[InlineData("company")]
|
|
[InlineData("foreign-company")]
|
|
[InlineData("location")]
|
|
[InlineData("follow-up")]
|
|
[InlineData("needs-work")]
|
|
[InlineData("interview")]
|
|
[InlineData("deleted")]
|
|
[InlineData("include-deleted")]
|
|
public async Task Table_pages_and_board_have_identical_owned_membership(string scenario)
|
|
{
|
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options;
|
|
var user = new Mock<ICurrentUserService>();
|
|
user.SetupGet(x => x.UserId).Returns("owner");
|
|
await using var db = new JobTrackerContext(options, user.Object);
|
|
await db.Database.EnsureCreatedAsync();
|
|
db.Users.AddRange(new ApplicationUser { Id = "owner", UserName = "owner" }, new ApplicationUser { Id = "other", UserName = "other" });
|
|
await db.SaveChangesAsync();
|
|
var company = new Company { Name = "Nordlys", OwnerUserId = "owner" };
|
|
var jobs = Enumerable.Range(0, 62).Select(i => new JobApplication
|
|
{
|
|
Company = company, OwnerUserId = "owner", JobTitle = i == 0 ? "Seniorrådgiver" : $"Board {i:D2}",
|
|
Status = i == 1 ? "Custom review" : i % 3 == 0 ? "Interview" : "Applied",
|
|
Location = i % 2 == 0 ? "Tromsø" : "Oslo", IsDeleted = i == 61,
|
|
DateApplied = DateTime.UtcNow.Date.AddDays(-20), Notes = i == 2 ? "note-needle" : null,
|
|
}).ToList();
|
|
db.JobApplications.AddRange(jobs);
|
|
await db.SaveChangesAsync();
|
|
db.Correspondences.Add(new Correspondence { JobApplicationId = jobs[3].Id, Content = "mail-needle", From = "Company", Date = DateTime.UtcNow.AddDays(-19) });
|
|
await db.SaveChangesAsync();
|
|
user.SetupGet(x => x.UserId).Returns("other");
|
|
var foreign = new JobApplication { Company = new Company { Name = "Foreign", OwnerUserId = "other" }, OwnerUserId = "other", JobTitle = "Board private", Status = "Applied" };
|
|
db.JobApplications.Add(foreign);
|
|
await db.SaveChangesAsync();
|
|
user.SetupGet(x => x.UserId).Returns("owner");
|
|
|
|
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), TestHostFactory.CreateUserManager().Object)
|
|
{
|
|
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "owner") }, "test")) } }
|
|
};
|
|
var q = scenario switch { "search" or "combined" => " Board ", "notes" => "note-needle", "correspondence" => "mail-needle", "none" => "absent", _ => null };
|
|
var status = scenario switch { "standard" or "combined" => "Applied", "custom" => "Custom review", _ => null };
|
|
int? companyId = scenario == "company" ? company.Id : scenario == "foreign-company" ? foreign.CompanyId : null;
|
|
var location = scenario == "location" ? "Tromsø" : null;
|
|
var follow = scenario == "follow-up";
|
|
var readiness = scenario is "needs-work" or "interview" ? scenario : null;
|
|
var deleted = scenario == "deleted";
|
|
var includeDeleted = deleted || scenario == "include-deleted";
|
|
var board = Assert.IsType<List<JobApplication>>(Assert.IsType<OkObjectResult>((await controller.GetBoard(
|
|
includeDeleted: includeDeleted, q: q, status: status, companyId: companyId, location: location,
|
|
needsFollowUp: follow, readiness: readiness, deletedOnly: deleted, sortBy: "dateApplied", sortDir: "desc")).Result).Value);
|
|
var ids = new List<int>();
|
|
var page = 1;
|
|
int total;
|
|
do
|
|
{
|
|
var result = await controller.GetAll(page: page++, pageSize: 15, q: q, status: status, companyId: companyId,
|
|
location: location, needsFollowUp: follow, readiness: readiness, includeDeleted: includeDeleted,
|
|
deletedOnly: deleted, sortBy: "dateApplied", sortDir: "desc");
|
|
var data = Assert.IsType<PagedResult<JobApplicationDto>>(Assert.IsType<OkObjectResult>(result.Result).Value);
|
|
Assert.InRange(data.Items.Count, 0, 15);
|
|
total = data.Total;
|
|
if (ids.Count < total) Assert.NotEmpty(data.Items); // fail rather than loop on a broken page
|
|
ids.AddRange(data.Items.Select(x => x.Id));
|
|
} while (ids.Count < total);
|
|
Assert.Equal(total, board.Count);
|
|
Assert.Equal(ids, board.Select(x => x.Id)); // includes stable ordering across tied dates/pages
|
|
Assert.Equal(ids.Count, ids.Distinct().Count());
|
|
Assert.DoesNotContain(foreign.Id, ids);
|
|
if (scenario is "none" or "foreign-company") Assert.Empty(ids);
|
|
else Assert.NotEmpty(ids); // parity must not pass vacuously for derived filters
|
|
if (scenario == "all") Assert.Equal(61, total);
|
|
if (scenario == "search") Assert.Equal(60, total);
|
|
if (scenario is "notes" or "correspondence" or "custom" or "deleted") Assert.Single(ids);
|
|
if (scenario == "include-deleted") Assert.Equal(62, total);
|
|
Assert.IsType<BadRequestObjectResult>((await controller.GetBoard(readiness: "invented")).Result);
|
|
Assert.IsType<BadRequestObjectResult>((await controller.GetAll(readiness: "invented")).Result);
|
|
}
|
|
}
|