feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using System.Text.Json;
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class OperationsControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Operation_api_lists_mutates_and_hides_other_owner_records()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
Guid ownId;
|
||||
Guid otherId;
|
||||
await using (var db = fixture.Context("user-1"))
|
||||
ownId = (await fixture.Operations(db).CreateAsync(Request("own"), default)).Operation.Id;
|
||||
await using (var db = fixture.Context("user-2"))
|
||||
otherId = (await fixture.Operations(db).CreateAsync(Request("other"), default)).Operation.Id;
|
||||
|
||||
await using var ownerDb = fixture.Context("user-1");
|
||||
var controller = fixture.OperationsController(ownerDb);
|
||||
var listed = Assert.IsType<OkObjectResult>((await controller.List(cancellationToken: default)).Result);
|
||||
var item = Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<OperationDto>>(listed.Value));
|
||||
Assert.Equal(ownId, item.Id);
|
||||
Assert.IsType<NotFoundResult>((await controller.Get(otherId, default)).Result);
|
||||
|
||||
var cancelled = Assert.IsType<OkObjectResult>((await controller.Cancel(ownId, default)).Result);
|
||||
Assert.Equal(OperationStatuses.Cancelled, Assert.IsType<OperationDto>(cancelled.Value).Status);
|
||||
Assert.IsType<ConflictObjectResult>((await controller.Cancel(ownId, default)).Result);
|
||||
var retried = Assert.IsType<OkObjectResult>((await controller.Retry(ownId, default)).Result);
|
||||
Assert.Equal(OperationStatuses.Queued, Assert.IsType<OperationDto>(retried.Value).Status);
|
||||
Assert.IsType<ConflictObjectResult>((await controller.Retry(ownId, default)).Result);
|
||||
|
||||
var serialized = JsonSerializer.Serialize(item);
|
||||
Assert.DoesNotContain("IdempotencyKey", serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LeaseToken", serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("FailureMessage", serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ResultReference", serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Provider", serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Model", serialized, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Notification_api_is_owner_scoped_and_read_dismiss_are_idempotent()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
Guid notificationId;
|
||||
await using (var db = fixture.Context("user-1"))
|
||||
{
|
||||
var operations = fixture.Operations(db);
|
||||
var operation = await operations.CreateAsync(Request("notification"), default);
|
||||
Assert.True(await operations.RequestCancellationAsync(operation.Operation.Id, default));
|
||||
notificationId = (await db.UserNotifications.AsNoTracking().SingleAsync()).Id;
|
||||
}
|
||||
|
||||
await using (var otherDb = fixture.Context("user-2"))
|
||||
{
|
||||
var other = fixture.NotificationsController(otherDb);
|
||||
Assert.Empty(Assert.IsAssignableFrom<IReadOnlyList<NotificationDto>>(
|
||||
Assert.IsType<OkObjectResult>((await other.List(cancellationToken: default)).Result).Value));
|
||||
Assert.IsType<NotFoundResult>(await other.MarkRead(notificationId, default));
|
||||
Assert.IsType<NotFoundResult>(await other.Dismiss(notificationId, default));
|
||||
}
|
||||
|
||||
await using var ownerDb = fixture.Context("user-1");
|
||||
var controller = fixture.NotificationsController(ownerDb);
|
||||
Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<NotificationDto>>(
|
||||
Assert.IsType<OkObjectResult>((await controller.List(cancellationToken: default)).Result).Value));
|
||||
Assert.IsType<NoContentResult>(await controller.MarkRead(notificationId, default));
|
||||
Assert.IsType<NoContentResult>(await controller.MarkRead(notificationId, default));
|
||||
Assert.IsType<NoContentResult>(await controller.Dismiss(notificationId, default));
|
||||
Assert.IsType<NoContentResult>(await controller.Dismiss(notificationId, default));
|
||||
Assert.Empty(Assert.IsAssignableFrom<IReadOnlyList<NotificationDto>>(
|
||||
Assert.IsType<OkObjectResult>((await controller.List(cancellationToken: default)).Result).Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Operation_and_notification_endpoints_validate_bounds_and_require_local_auth()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
await using var db = fixture.Context("user-1");
|
||||
Assert.IsType<BadRequestObjectResult>((await fixture.OperationsController(db).List(0, default)).Result);
|
||||
Assert.IsType<BadRequestObjectResult>((await fixture.NotificationsController(db).List(101, default)).Result);
|
||||
|
||||
foreach (var type in new[] { typeof(OperationsController), typeof(NotificationsController) })
|
||||
{
|
||||
var authorize = Assert.Single(type.GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true).Cast<AuthorizeAttribute>());
|
||||
Assert.Equal("local", authorize.AuthenticationSchemes);
|
||||
}
|
||||
}
|
||||
|
||||
private static CreateUserOperation Request(string key) => new("synthetic-test", key, "authorized", "local-only", "job", "42");
|
||||
|
||||
private sealed class CurrentUser(string? userId) : ICurrentUserService
|
||||
{
|
||||
public string? UserId { get; } = userId;
|
||||
}
|
||||
|
||||
private sealed class Fixture : IAsyncDisposable
|
||||
{
|
||||
private readonly string _root;
|
||||
private readonly string _connectionString;
|
||||
|
||||
private Fixture(string root)
|
||||
{
|
||||
_root = root;
|
||||
_connectionString = $"Data Source={Path.Combine(root, "api.db")};Default Timeout=5;Pooling=False";
|
||||
}
|
||||
|
||||
public static async Task<Fixture> CreateAsync()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"jobtracker-operation-api-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
var fixture = new Fixture(root);
|
||||
await using var db = fixture.Context(null);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
public JobTrackerContext Context(string? owner)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(_connectionString).Options;
|
||||
return new JobTrackerContext(options, new CurrentUser(owner));
|
||||
}
|
||||
|
||||
public UserOperationStore Operations(JobTrackerContext db) => new(db, TimeProvider.System);
|
||||
public UserNotificationStore Notifications(JobTrackerContext db) => new(db, TimeProvider.System);
|
||||
public OperationsController OperationsController(JobTrackerContext db) => new(Operations(db));
|
||||
public NotificationsController NotificationsController(JobTrackerContext db) => new(Notifications(db));
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (Directory.Exists(_root)) Directory.Delete(_root, true);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user