2fa4e38ba9
Export readable private draft content only through existing owner-filtered encrypted and daily export boundaries, with cross-tenant regression coverage.
113 lines
4.8 KiB
C#
113 lines
4.8 KiB
C#
using System.Reflection;
|
|
using JobTrackerApi.Controllers;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Tests.TestSupport;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
public sealed class BackupControllerTests
|
|
{
|
|
[Fact]
|
|
public void Backup_controller_requires_local_authorization()
|
|
{
|
|
var attribute = typeof(BackupController).GetCustomAttribute<AuthorizeAttribute>();
|
|
Assert.NotNull(attribute);
|
|
Assert.Equal("local", attribute!.AuthenticationSchemes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Export_controller_requires_local_authorization()
|
|
{
|
|
var attribute = typeof(ExportController).GetCustomAttribute<AuthorizeAttribute>();
|
|
Assert.NotNull(attribute);
|
|
Assert.Equal("local", attribute!.AuthenticationSchemes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Encrypted_returns_file_payload_on_non_windows_platforms_too()
|
|
{
|
|
await using var db = CreateDb();
|
|
db.Companies.Add(new Company { Name = "Acme", OwnerUserId = "user-1" });
|
|
var job = new JobApplication { JobTitle = "Backend Developer", OwnerUserId = "user-1" };
|
|
var otherJob = new JobApplication { JobTitle = "Other tenant role", OwnerUserId = "user-2" };
|
|
db.JobApplications.AddRange(job, otherJob);
|
|
await db.SaveChangesAsync();
|
|
db.EmailSendAttempts.Add(new EmailSendAttempt
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = "user-1",
|
|
JobApplicationId = job.Id,
|
|
Provider = "gmail",
|
|
ClientRequestId = Guid.NewGuid().ToString(),
|
|
PayloadHash = new string('a', 64),
|
|
Status = EmailSendStatuses.Sent,
|
|
ProviderMessageId = "synthetic-message-id",
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
CompletedAtUtc = DateTime.UtcNow,
|
|
});
|
|
db.EmailDrafts.Add(new EmailDraft
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = "user-1",
|
|
JobApplicationId = job.Id,
|
|
Provider = "gmail",
|
|
To = "recipient@example.test",
|
|
Subject = "Synthetic export subject",
|
|
BodyText = "Synthetic readable draft body.",
|
|
ThreadId = "synthetic-thread",
|
|
Revision = 3,
|
|
CreatedAtUtc = DateTime.UtcNow.AddMinutes(-5),
|
|
UpdatedAtUtc = DateTime.UtcNow,
|
|
});
|
|
db.EmailDrafts.Add(new EmailDraft
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = "user-2",
|
|
JobApplicationId = otherJob.Id,
|
|
Provider = "microsoft",
|
|
To = "other@example.test",
|
|
Subject = "Other tenant subject",
|
|
BodyText = "Other tenant private body.",
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
UpdatedAtUtc = DateTime.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var provider = DataProtectionProvider.Create(new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"jobtracker-tests-{Guid.NewGuid():N}")));
|
|
var controller = new BackupController(db, new NullLogger<BackupController>(), provider);
|
|
|
|
var result = await controller.Encrypted(CancellationToken.None);
|
|
|
|
var file = Assert.IsType<FileContentResult>(result);
|
|
Assert.Equal("application/octet-stream", file.ContentType);
|
|
Assert.EndsWith(".jtbackup", file.FileDownloadName);
|
|
Assert.NotEmpty(file.FileContents);
|
|
|
|
var protectedText = System.Text.Encoding.UTF8.GetString(file.FileContents);
|
|
var json = Convert.FromBase64String(provider.CreateProtector("JobTrackerApi.Backup.v1").Unprotect(protectedText));
|
|
using var document = System.Text.Json.JsonDocument.Parse(json);
|
|
var attempt = Assert.Single(document.RootElement.GetProperty("Data").GetProperty("EmailSendAttempts").EnumerateArray());
|
|
Assert.Equal("gmail", attempt.GetProperty("Provider").GetString());
|
|
Assert.Equal("sent", attempt.GetProperty("Status").GetString());
|
|
Assert.False(attempt.TryGetProperty("PayloadHash", out _));
|
|
var draft = Assert.Single(document.RootElement.GetProperty("Data").GetProperty("EmailDrafts").EnumerateArray());
|
|
Assert.Equal("recipient@example.test", draft.GetProperty("To").GetString());
|
|
Assert.Equal("Synthetic export subject", draft.GetProperty("Subject").GetString());
|
|
Assert.Equal("Synthetic readable draft body.", draft.GetProperty("BodyText").GetString());
|
|
Assert.Equal("synthetic-thread", draft.GetProperty("ThreadId").GetString());
|
|
Assert.Equal(3, draft.GetProperty("Revision").GetInt64());
|
|
}
|
|
|
|
private static JobTrackerContext CreateDb()
|
|
{
|
|
return TestHostFactory.CreateInMemoryDb();
|
|
}
|
|
}
|