Files
jobtrackingapp/JobTrackerApi.Tests/ApplicationCalendarControllerTests.cs
T

190 lines
9.9 KiB
C#

using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Data.Sqlite;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class ApplicationCalendarControllerTests
{
[Fact]
public async Task Agenda_date_window_translates_on_sqlite_and_keeps_legacy_wall_clock_date()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var current = new Mock<ICurrentUserService>(); current.SetupGet(service => service.UserId).Returns("owner-1");
await using var db = new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options, current.Object);
await db.Database.EnsureCreatedAsync();
var job = await SeedJobAsync(db, "owner-1");
job.Status = "Saved"; job.FollowUpAt = null; job.Deadline = new DateTime(2026, 9, 30, 23, 59, 59);
await db.SaveChangesAsync(); db.ChangeTracker.Clear();
var controller = CreateController(db, "owner-1", new Mock<IGmailOAuthService>(MockBehavior.Strict).Object, new Mock<IMicrosoftGraphOAuthService>(MockBehavior.Strict).Object);
var result = await controller.Agenda(new DateOnly(2026, 9, 1), new DateOnly(2026, 9, 30), default);
var row = Assert.Single(Assert.IsAssignableFrom<IEnumerable<ApplicationCalendarController.CalendarAgendaItemDto>>(Assert.IsType<OkObjectResult>(result.Result).Value));
Assert.Equal("2026-09-30", row.Date);
Assert.Equal(new DateTime(2026, 9, 30, 23, 59, 59), row.RecordedAt);
}
[Fact]
public async Task Agenda_includes_saved_deadlines_and_preserves_recorded_dates_without_provider_access()
{
await using var db = CreateDb("owner-1");
var job = await SeedJobAsync(db, "owner-1");
job.Status = "Saved";
job.FollowUpAt = new DateTime(2026, 9, 2, 0, 0, 0, DateTimeKind.Unspecified);
job.Deadline = new DateTime(2026, 9, 15, 23, 30, 0, DateTimeKind.Unspecified);
await db.SaveChangesAsync();
var controller = CreateController(db, "owner-1", new Mock<IGmailOAuthService>(MockBehavior.Strict).Object, new Mock<IMicrosoftGraphOAuthService>(MockBehavior.Strict).Object);
var response = await controller.Agenda(new DateOnly(2026, 9, 1), new DateOnly(2026, 9, 30), default);
var rows = Assert.IsAssignableFrom<IEnumerable<ApplicationCalendarController.CalendarAgendaItemDto>>(Assert.IsType<OkObjectResult>(response.Result).Value).ToList();
Assert.Equal(2, rows.Count);
Assert.Equal("follow-up", rows[0].Kind);
Assert.Equal("2026-09-02", rows[0].Date);
Assert.Equal("deadline", rows[1].Kind);
Assert.Equal("2026-09-15", rows[1].Date);
Assert.Equal(job.Deadline, rows[1].RecordedAt);
Assert.Equal(DateTimeKind.Unspecified, rows[1].RecordedAt.Kind);
Assert.All(rows, row => { Assert.Equal(job.Id, row.JobApplicationId); Assert.Equal("Acme", row.CompanyName); });
}
[Fact]
public async Task Agenda_excludes_other_owners_deleted_and_out_of_range_dates()
{
await using var db = CreateDb("owner-1");
await SeedJobAsync(db, "owner-2");
var deleted = await SeedJobAsync(db, "owner-1"); deleted.IsDeleted = true;
var outside = await SeedJobAsync(db, "owner-1"); outside.FollowUpAt = new DateTime(2026, 8, 31); outside.Deadline = new DateTime(2026, 10, 1);
var edge = await SeedJobAsync(db, "owner-1"); edge.FollowUpAt = null; edge.Deadline = new DateTime(2026, 9, 30, 23, 59, 59);
await db.SaveChangesAsync();
var controller = CreateController(db, "owner-1", Mock.Of<IGmailOAuthService>(), Mock.Of<IMicrosoftGraphOAuthService>());
var response = await controller.Agenda(new DateOnly(2026, 9, 1), new DateOnly(2026, 9, 30), default);
var rows = Assert.IsAssignableFrom<IEnumerable<ApplicationCalendarController.CalendarAgendaItemDto>>(Assert.IsType<OkObjectResult>(response.Result).Value);
Assert.Equal(edge.Id, Assert.Single(rows).JobApplicationId);
}
[Theory]
[InlineData("2026-09-30", "2026-09-01")]
[InlineData("2026-01-01", "2026-12-31")]
[InlineData("9999-12-01", "9999-12-31")]
public async Task Agenda_rejects_unbounded_or_invalid_ranges(string from, string to)
{
await using var db = CreateDb("owner-1");
var controller = CreateController(db, "owner-1", Mock.Of<IGmailOAuthService>(), Mock.Of<IMicrosoftGraphOAuthService>());
var response = await controller.Agenda(DateOnly.Parse(from), DateOnly.Parse(to), default);
Assert.IsType<BadRequestObjectResult>(response.Result);
}
[Fact]
public async Task Status_distinguishes_connection_from_calendar_consent()
{
await using var db = CreateDb("owner-1");
var google = new Mock<IGmailOAuthService>();
google.Setup(service => service.GetConnectionAsync("owner-1", It.IsAny<CancellationToken>()))
.ReturnsAsync(new GmailConnection { OwnerUserId = "owner-1", Scope = GmailOAuthService.CalendarScope });
var microsoft = new Mock<IMicrosoftGraphOAuthService>();
microsoft.Setup(service => service.GetConnectionAsync("owner-1", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftGraphConnection { OwnerUserId = "owner-1", Scope = "Mail.Read" });
var controller = CreateController(db, "owner-1", google.Object, microsoft.Object);
var value = Assert.IsType<ApplicationCalendarController.CalendarStatusDto>(Assert.IsType<OkObjectResult>((await controller.Status(default)).Result).Value);
Assert.True(value.Google.Connected);
Assert.True(value.Google.Writable);
Assert.True(value.Microsoft.Connected);
Assert.False(value.Microsoft.Writable);
}
[Fact]
public async Task Creates_follow_up_in_selected_connected_calendar_with_stable_job_context()
{
await using var db = CreateDb("owner-1");
var job = await SeedJobAsync(db, "owner-1");
ExternalCalendarEventRequest? sent = null;
var google = new Mock<IGmailOAuthService>();
google.Setup(service => service.CreateCalendarEventAsync("owner-1", It.IsAny<ExternalCalendarEventRequest>(), It.IsAny<CancellationToken>()))
.Callback<string, ExternalCalendarEventRequest, CancellationToken>((_, request, _) => sent = request)
.ReturnsAsync(new ExternalCalendarEventResult("event-1", "https://calendar.google.test/event-1"));
var controller = CreateController(db, "owner-1", google.Object, Mock.Of<IMicrosoftGraphOAuthService>());
var response = await controller.CreateEvent(job.Id, new("google", "follow-up"), default);
var value = Assert.IsType<ApplicationCalendarController.CalendarEventDto>(Assert.IsType<OkObjectResult>(response.Result).Value);
Assert.Equal("google", value.Provider);
Assert.Equal("event-1", value.Id);
Assert.NotNull(sent);
Assert.Equal("Follow up: Backend Engineer at Acme", sent.Summary);
Assert.Equal(job.FollowUpAt, sent.StartsAtUtc.UtcDateTime);
Assert.False(sent.AllDay);
Assert.Equal(64, sent.StableId.Length);
}
[Fact]
public async Task Rejects_another_owners_job_without_calling_provider()
{
await using var db = CreateDb("owner-1");
var job = await SeedJobAsync(db, "owner-2");
var google = new Mock<IGmailOAuthService>(MockBehavior.Strict);
var controller = CreateController(db, "owner-1", google.Object, Mock.Of<IMicrosoftGraphOAuthService>());
var response = await controller.CreateEvent(job.Id, new("google", "follow-up"), default);
Assert.IsType<NotFoundResult>(response.Result);
}
[Fact]
public async Task Downloads_portable_all_day_deadline_event_without_provider_credentials()
{
await using var db = CreateDb("owner-1");
var job = await SeedJobAsync(db, "owner-1");
var controller = CreateController(db, "owner-1", Mock.Of<IGmailOAuthService>(), Mock.Of<IMicrosoftGraphOAuthService>());
var response = Assert.IsType<FileContentResult>(await controller.DownloadEvent(job.Id, "deadline", default));
var content = System.Text.Encoding.UTF8.GetString(response.FileContents);
Assert.Equal("text/calendar; charset=utf-8", response.ContentType);
Assert.Contains("DTSTART;VALUE=DATE:20260915", content);
Assert.Contains("DTEND;VALUE=DATE:20260916", content);
Assert.Contains("SUMMARY:Application deadline: Backend Engineer at Acme", content);
}
private static JobTrackerContext CreateDb(string owner)
{
var current = new Mock<ICurrentUserService>();
current.SetupGet(service => service.UserId).Returns(owner);
return new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options, current.Object);
}
private static async Task<JobApplication> SeedJobAsync(JobTrackerContext db, string owner)
{
var company = new Company { OwnerUserId = owner, Name = "Acme" };
var job = new JobApplication
{
OwnerUserId = owner,
Company = company,
JobTitle = "Backend Engineer",
Status = "Applied",
FollowUpAt = new DateTime(2026, 9, 2, 10, 30, 0, DateTimeKind.Utc),
Deadline = new DateTime(2026, 9, 15, 0, 0, 0, DateTimeKind.Utc),
Location = "Oslo"
};
db.JobApplications.Add(job);
await db.SaveChangesAsync();
return job;
}
private static ApplicationCalendarController CreateController(JobTrackerContext db, string owner, IGmailOAuthService google, IMicrosoftGraphOAuthService microsoft)
{
var current = new Mock<ICurrentUserService>();
current.SetupGet(service => service.UserId).Returns(owner);
return new ApplicationCalendarController(db, current.Object, google, microsoft);
}
}