Files
RealCV/tests/RealCV.Tests/Services/CVCheckServiceTests.cs
Peter Foster 998e9a8ab8 Rename project to RealCV with new logo and font updates
- Rename all TrueCV references to RealCV across the codebase
- Add new transparent RealCV logo
- Switch from JetBrains Mono to Inter font for better number clarity
- Update solution, project files, and namespaces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 15:07:20 +00:00

728 lines
21 KiB
C#

using System.Linq.Expressions;
using System.Text.Json;
using FluentAssertions;
using Hangfire;
using Hangfire.Common;
using Hangfire.States;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using RealCV.Application.DTOs;
using RealCV.Application.Interfaces;
using RealCV.Application.Models;
using RealCV.Domain.Entities;
using RealCV.Domain.Enums;
using RealCV.Infrastructure.Data;
using RealCV.Infrastructure.Jobs;
using RealCV.Infrastructure.Services;
namespace RealCV.Tests.Services;
public sealed class CVCheckServiceTests : IDisposable
{
private readonly ApplicationDbContext _dbContext;
private readonly Mock<IFileStorageService> _fileStorageServiceMock;
private readonly Mock<IBackgroundJobClient> _backgroundJobClientMock;
private readonly Mock<IAuditService> _auditServiceMock;
private readonly Mock<ISubscriptionService> _subscriptionServiceMock;
private readonly Mock<ILogger<CVCheckService>> _loggerMock;
private readonly CVCheckService _sut;
public CVCheckServiceTests()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_dbContext = new ApplicationDbContext(options);
_fileStorageServiceMock = new Mock<IFileStorageService>();
_backgroundJobClientMock = new Mock<IBackgroundJobClient>();
_auditServiceMock = new Mock<IAuditService>();
_subscriptionServiceMock = new Mock<ISubscriptionService>();
_loggerMock = new Mock<ILogger<CVCheckService>>();
// Setup subscription service to allow checks by default
_subscriptionServiceMock.Setup(x => x.CanPerformCheckAsync(It.IsAny<Guid>()))
.ReturnsAsync(true);
_sut = new CVCheckService(
_dbContext,
_fileStorageServiceMock.Object,
_backgroundJobClientMock.Object,
_auditServiceMock.Object,
_subscriptionServiceMock.Object,
_loggerMock.Object);
}
public void Dispose()
{
_dbContext.Dispose();
}
#region CreateCheckAsync Tests
[Fact]
public async Task CreateCheckAsync_CreatesRecordWithCorrectData()
{
// Arrange
var userId = Guid.NewGuid();
var fileName = "test-cv.pdf";
var expectedBlobUrl = "https://storage.blob.core.windows.net/cvs/test-cv.pdf";
using var stream = new MemoryStream();
_fileStorageServiceMock
.Setup(x => x.UploadAsync(It.IsAny<Stream>(), fileName))
.ReturnsAsync(expectedBlobUrl);
// Act
var checkId = await _sut.CreateCheckAsync(userId, stream, fileName);
// Assert
var savedCheck = await _dbContext.CVChecks.FirstOrDefaultAsync(c => c.Id == checkId);
savedCheck.Should().NotBeNull();
savedCheck!.UserId.Should().Be(userId);
savedCheck.OriginalFileName.Should().Be(fileName);
savedCheck.BlobUrl.Should().Be(expectedBlobUrl);
savedCheck.Status.Should().Be(CheckStatus.Pending);
savedCheck.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public async Task CreateCheckAsync_UploadsFileToBlobStorage()
{
// Arrange
var userId = Guid.NewGuid();
var fileName = "resume.docx";
var expectedBlobUrl = "https://storage.blob.core.windows.net/cvs/resume.docx";
using var stream = new MemoryStream(new byte[] { 0x01, 0x02, 0x03 });
_fileStorageServiceMock
.Setup(x => x.UploadAsync(stream, fileName))
.ReturnsAsync(expectedBlobUrl);
// Act
await _sut.CreateCheckAsync(userId, stream, fileName);
// Assert
_fileStorageServiceMock.Verify(
x => x.UploadAsync(stream, fileName),
Times.Once);
}
[Fact]
public async Task CreateCheckAsync_QueuesHangfireBackgroundJob()
{
// Arrange
var userId = Guid.NewGuid();
var fileName = "cv.pdf";
using var stream = new MemoryStream();
_fileStorageServiceMock
.Setup(x => x.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>()))
.ReturnsAsync("https://blob.url/cv.pdf");
// Act
var checkId = await _sut.CreateCheckAsync(userId, stream, fileName);
// Assert
_backgroundJobClientMock.Verify(
x => x.Create(
It.Is<Job>(job =>
job.Type == typeof(ProcessCVCheckJob) &&
job.Method.Name == nameof(ProcessCVCheckJob.ExecuteAsync)),
It.IsAny<EnqueuedState>()),
Times.Once);
}
[Fact]
public async Task CreateCheckAsync_ReturnsNewCheckId()
{
// Arrange
var userId = Guid.NewGuid();
var fileName = "my-cv.pdf";
using var stream = new MemoryStream();
_fileStorageServiceMock
.Setup(x => x.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>()))
.ReturnsAsync("https://blob.url/my-cv.pdf");
// Act
var checkId = await _sut.CreateCheckAsync(userId, stream, fileName);
// Assert
checkId.Should().NotBe(Guid.Empty);
var savedCheck = await _dbContext.CVChecks.FirstOrDefaultAsync(c => c.Id == checkId);
savedCheck.Should().NotBeNull();
}
[Fact]
public async Task CreateCheckAsync_ThrowsArgumentNullException_WhenFileIsNull()
{
// Arrange
var userId = Guid.NewGuid();
var fileName = "test.pdf";
// Act
var act = () => _sut.CreateCheckAsync(userId, null!, fileName);
// Assert
await act.Should().ThrowAsync<ArgumentNullException>()
.WithParameterName("file");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task CreateCheckAsync_ThrowsArgumentException_WhenFileNameIsNullOrWhitespace(string? fileName)
{
// Arrange
var userId = Guid.NewGuid();
using var stream = new MemoryStream();
// Act
var act = () => _sut.CreateCheckAsync(userId, stream, fileName!);
// Assert
await act.Should().ThrowAsync<ArgumentException>()
.WithParameterName("fileName");
}
#endregion
#region GetCheckAsync Tests
[Fact]
public async Task GetCheckAsync_ReturnsNull_WhenCheckDoesNotExist()
{
// Arrange
var nonExistentId = Guid.NewGuid();
// Act
var result = await _sut.GetCheckAsync(nonExistentId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetCheckAsync_ReturnsCorrectDto_WhenCheckExists()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
var createdAt = DateTime.UtcNow.AddHours(-1);
var completedAt = DateTime.UtcNow.AddMinutes(-30);
var cvCheck = new CVCheck
{
Id = checkId,
UserId = userId,
OriginalFileName = "professional-cv.pdf",
BlobUrl = "https://blob.url/professional-cv.pdf",
Status = CheckStatus.Completed,
VeracityScore = 85,
CreatedAt = createdAt,
CompletedAt = completedAt
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetCheckAsync(checkId);
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(checkId);
result.OriginalFileName.Should().Be("professional-cv.pdf");
result.Status.Should().Be("Completed");
result.VeracityScore.Should().Be(85);
result.CompletedAt.Should().Be(completedAt);
}
[Fact]
public async Task GetCheckAsync_ReturnsPendingStatus_ForNewCheck()
{
// Arrange
var checkId = Guid.NewGuid();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = Guid.NewGuid(),
OriginalFileName = "new-cv.pdf",
BlobUrl = "https://blob.url/new-cv.pdf",
Status = CheckStatus.Pending,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetCheckAsync(checkId);
// Assert
result.Should().NotBeNull();
result!.Status.Should().Be("Pending");
result.VeracityScore.Should().BeNull();
result.CompletedAt.Should().BeNull();
}
#endregion
#region GetUserChecksAsync Tests
[Fact]
public async Task GetUserChecksAsync_ReturnsEmptyList_WhenUserHasNoChecks()
{
// Arrange
var userId = Guid.NewGuid();
// Act
var result = await _sut.GetUserChecksAsync(userId);
// Assert
result.Should().NotBeNull();
result.Should().BeEmpty();
}
[Fact]
public async Task GetUserChecksAsync_ReturnsAllChecksForUser()
{
// Arrange
var userId = Guid.NewGuid();
var check1 = new CVCheck
{
Id = Guid.NewGuid(),
UserId = userId,
OriginalFileName = "cv1.pdf",
BlobUrl = "https://blob.url/cv1.pdf",
Status = CheckStatus.Completed,
VeracityScore = 90,
CreatedAt = DateTime.UtcNow.AddDays(-2)
};
var check2 = new CVCheck
{
Id = Guid.NewGuid(),
UserId = userId,
OriginalFileName = "cv2.pdf",
BlobUrl = "https://blob.url/cv2.pdf",
Status = CheckStatus.Pending,
CreatedAt = DateTime.UtcNow.AddDays(-1)
};
var check3 = new CVCheck
{
Id = Guid.NewGuid(),
UserId = userId,
OriginalFileName = "cv3.pdf",
BlobUrl = "https://blob.url/cv3.pdf",
Status = CheckStatus.Processing,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.AddRange(check1, check2, check3);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetUserChecksAsync(userId);
// Assert
result.Should().HaveCount(3);
result.Select(c => c.OriginalFileName)
.Should().Contain(new[] { "cv1.pdf", "cv2.pdf", "cv3.pdf" });
}
[Fact]
public async Task GetUserChecksAsync_DoesNotReturnOtherUsersChecks()
{
// Arrange
var userId1 = Guid.NewGuid();
var userId2 = Guid.NewGuid();
var user1Check = new CVCheck
{
Id = Guid.NewGuid(),
UserId = userId1,
OriginalFileName = "user1-cv.pdf",
BlobUrl = "https://blob.url/user1-cv.pdf",
Status = CheckStatus.Completed,
CreatedAt = DateTime.UtcNow.AddHours(-1)
};
var user2Check1 = new CVCheck
{
Id = Guid.NewGuid(),
UserId = userId2,
OriginalFileName = "user2-cv-a.pdf",
BlobUrl = "https://blob.url/user2-cv-a.pdf",
Status = CheckStatus.Pending,
CreatedAt = DateTime.UtcNow.AddHours(-2)
};
var user2Check2 = new CVCheck
{
Id = Guid.NewGuid(),
UserId = userId2,
OriginalFileName = "user2-cv-b.pdf",
BlobUrl = "https://blob.url/user2-cv-b.pdf",
Status = CheckStatus.Completed,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.AddRange(user1Check, user2Check1, user2Check2);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetUserChecksAsync(userId1);
// Assert
result.Should().HaveCount(1);
result.Single().OriginalFileName.Should().Be("user1-cv.pdf");
result.Should().NotContain(c => c.OriginalFileName.StartsWith("user2"));
}
[Fact]
public async Task GetUserChecksAsync_ReturnsChecksOrderedByCreatedAtDescending()
{
// Arrange
var userId = Guid.NewGuid();
// Create checks with specific IDs so we can update them after save
var oldestId = Guid.NewGuid();
var middleId = Guid.NewGuid();
var newestId = Guid.NewGuid();
var oldestCheck = new CVCheck
{
Id = oldestId,
UserId = userId,
OriginalFileName = "oldest.pdf",
BlobUrl = "https://blob.url/oldest.pdf",
Status = CheckStatus.Completed
};
var middleCheck = new CVCheck
{
Id = middleId,
UserId = userId,
OriginalFileName = "middle.pdf",
BlobUrl = "https://blob.url/middle.pdf",
Status = CheckStatus.Completed
};
var newestCheck = new CVCheck
{
Id = newestId,
UserId = userId,
OriginalFileName = "newest.pdf",
BlobUrl = "https://blob.url/newest.pdf",
Status = CheckStatus.Pending
};
// Add in random order - SaveChangesAsync will set CreatedAt to DateTime.UtcNow
_dbContext.CVChecks.AddRange(middleCheck, newestCheck, oldestCheck);
await _dbContext.SaveChangesAsync();
// Detach all entities to allow direct update
_dbContext.ChangeTracker.Clear();
// Now update CreatedAt timestamps using raw SQL to bypass SaveChangesAsync override
// Since InMemory database doesn't support raw SQL, we modify directly
var oldest = await _dbContext.CVChecks.FindAsync(oldestId);
var middle = await _dbContext.CVChecks.FindAsync(middleId);
var newest = await _dbContext.CVChecks.FindAsync(newestId);
oldest!.CreatedAt = DateTime.UtcNow.AddDays(-7);
middle!.CreatedAt = DateTime.UtcNow.AddDays(-3);
newest!.CreatedAt = DateTime.UtcNow;
// Mark as modified (not added) to bypass the CreatedAt override
_dbContext.Entry(oldest).State = EntityState.Modified;
_dbContext.Entry(middle).State = EntityState.Modified;
_dbContext.Entry(newest).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
_dbContext.ChangeTracker.Clear();
// Act
var result = await _sut.GetUserChecksAsync(userId);
// Assert
result.Should().HaveCount(3);
result[0].OriginalFileName.Should().Be("newest.pdf");
result[1].OriginalFileName.Should().Be("middle.pdf");
result[2].OriginalFileName.Should().Be("oldest.pdf");
}
#endregion
#region GetCheckForUserAsync Tests
[Fact]
public async Task GetCheckForUserAsync_ReturnsNull_WhenCheckDoesNotExist()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
// Act
var result = await _sut.GetCheckForUserAsync(checkId, userId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetCheckForUserAsync_ReturnsNull_WhenCheckBelongsToDifferentUser()
{
// Arrange
var checkId = Guid.NewGuid();
var ownerId = Guid.NewGuid();
var requestingUserId = Guid.NewGuid();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = ownerId,
OriginalFileName = "owner-cv.pdf",
BlobUrl = "https://blob.url/owner-cv.pdf",
Status = CheckStatus.Completed,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetCheckForUserAsync(checkId, requestingUserId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetCheckForUserAsync_ReturnsCheck_WhenCheckBelongsToUser()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = userId,
OriginalFileName = "my-cv.pdf",
BlobUrl = "https://blob.url/my-cv.pdf",
Status = CheckStatus.Completed,
VeracityScore = 88,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetCheckForUserAsync(checkId, userId);
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(checkId);
result.OriginalFileName.Should().Be("my-cv.pdf");
result.VeracityScore.Should().Be(88);
}
#endregion
#region GetReportAsync Tests
[Fact]
public async Task GetReportAsync_ReturnsNull_WhenCheckDoesNotExist()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
// Act
var result = await _sut.GetReportAsync(checkId, userId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetReportAsync_ReturnsNull_WhenCheckBelongsToDifferentUser()
{
// Arrange
var checkId = Guid.NewGuid();
var ownerId = Guid.NewGuid();
var requestingUserId = Guid.NewGuid();
var report = CreateTestReport();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = ownerId,
OriginalFileName = "cv.pdf",
BlobUrl = "https://blob.url/cv.pdf",
Status = CheckStatus.Completed,
ReportJson = JsonSerializer.Serialize(report),
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetReportAsync(checkId, requestingUserId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetReportAsync_ReturnsNull_WhenCheckIsNotCompleted()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = userId,
OriginalFileName = "cv.pdf",
BlobUrl = "https://blob.url/cv.pdf",
Status = CheckStatus.Processing,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetReportAsync(checkId, userId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetReportAsync_ReturnsNull_WhenReportJsonIsEmpty()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = userId,
OriginalFileName = "cv.pdf",
BlobUrl = "https://blob.url/cv.pdf",
Status = CheckStatus.Completed,
ReportJson = string.Empty,
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetReportAsync(checkId, userId);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetReportAsync_ReturnsReport_WhenCheckIsCompletedWithValidReport()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
var report = CreateTestReport();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = userId,
OriginalFileName = "cv.pdf",
BlobUrl = "https://blob.url/cv.pdf",
Status = CheckStatus.Completed,
ReportJson = JsonSerializer.Serialize(report),
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetReportAsync(checkId, userId);
// Assert
result.Should().NotBeNull();
result!.OverallScore.Should().Be(85);
result.ScoreLabel.Should().Be("Good");
}
[Fact]
public async Task GetReportAsync_ReturnsNull_WhenReportJsonIsInvalid()
{
// Arrange
var checkId = Guid.NewGuid();
var userId = Guid.NewGuid();
var cvCheck = new CVCheck
{
Id = checkId,
UserId = userId,
OriginalFileName = "cv.pdf",
BlobUrl = "https://blob.url/cv.pdf",
Status = CheckStatus.Completed,
ReportJson = "{ invalid json }",
CreatedAt = DateTime.UtcNow
};
_dbContext.CVChecks.Add(cvCheck);
await _dbContext.SaveChangesAsync();
// Act
var result = await _sut.GetReportAsync(checkId, userId);
// Assert
result.Should().BeNull();
}
#endregion
#region Helper Methods
private static VeracityReport CreateTestReport()
{
return new VeracityReport
{
OverallScore = 85,
ScoreLabel = "Good",
EmploymentVerifications = [],
TimelineAnalysis = new TimelineAnalysisResult
{
Gaps = [],
Overlaps = [],
TotalGapMonths = 0,
TotalOverlapMonths = 0
},
Flags = [],
GeneratedAt = DateTime.UtcNow
};
}
#endregion
}