This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Bsevita.Library/tests/Bsevita.Library.Api.Tests/LibraryWorkflowTests.cs
T
2026-06-24 08:33:23 +02:00

646 lines
29 KiB
C#

using Bsevita.Library.Api.Data.Generated.Entities;
using Bsevita.Library.Api.Infrastructure;
using Bsevita.Library.Api.Services;
using Bsevita.Library.Models.Books;
using Bsevita.Library.Models.Loans;
using Bsevita.Library.Models.Returns;
using Bsevita.Library.Models.Students;
namespace Bsevita.Library.Api.Tests;
public sealed class LibraryWorkflowTests
{
[Fact]
public async Task LoanAndReturn_UpdatesReportsAndAvailability()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
var returns = new ReturnService(database.Context, clock);
var reports = new ReportService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest
{
CardNumber = "S-1",
FirstName = "Ada",
LastName = "Lovelace",
ClassName = "4AHIT"
}, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest
{
BookNumber = "B-1",
Title = "Clean Architecture",
Author = "Robert C. Martin",
Subject = "Informatik"
}, CancellationToken.None);
var verification = await loans.VerifyAsync(new VerifyLoanRequest { CardNumber = "S-1", BookNumber = "B-1" }, CancellationToken.None);
Assert.True(verification.CanBorrow);
var loan = await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1" }, CancellationToken.None);
Assert.Equal(clock.GetUtcNow().AddDays(14), loan.DueAt);
Assert.Single(await reports.GetActiveLoansAsync(CancellationToken.None));
Assert.False((await books.GetByIdAsync(loan.BookId, CancellationToken.None)).IsAvailable);
var returned = await returns.ReturnAsync(new ReturnBookRequest { BookNumber = "B-1" }, CancellationToken.None);
Assert.Equal(loan.Id, returned.LoanId);
Assert.Empty(await reports.GetActiveLoansAsync(CancellationToken.None));
Assert.True((await books.GetByIdAsync(loan.BookId, CancellationToken.None)).IsAvailable);
}
[Fact]
public async Task CreateLoan_UsesCustomDueDate_WhenProvided()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest
{
CardNumber = "S-1",
FirstName = "Ada",
LastName = "Lovelace",
ClassName = "4AHIT"
}, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest
{
BookNumber = "B-1",
Title = "Clean Architecture",
Author = "Robert C. Martin",
Subject = "Informatik"
}, CancellationToken.None);
var dueAt = new DateTimeOffset(2026, 7, 20, 0, 0, 0, TimeSpan.Zero);
var loan = await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1", DueAt = dueAt }, CancellationToken.None);
Assert.Equal(dueAt, loan.DueAt);
}
[Fact]
public async Task DuplicateCardNumber_IsRejected()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var service = new StudentService(database.Context, new FixedTimeProvider(DateTimeOffset.UtcNow));
var request = new SaveStudentRequest { CardNumber = "same", FirstName = "A", LastName = "B", ClassName = "1A" };
await service.CreateAsync(request, CancellationToken.None);
await Assert.ThrowsAsync<Bsevita.Library.Api.Infrastructure.ResourceConflictException>(
() => service.CreateAsync(new SaveStudentRequest { CardNumber = "SAME", FirstName = "C", LastName = "D", ClassName = "1B" }, CancellationToken.None));
}
[Fact]
public async Task Statistics_CountsAvailableActiveBooks()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var now = new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero);
var student = new Student
{
StudentId = Guid.NewGuid(),
CardNumber = "S-1",
FirstName = "Ada",
LastName = "Lovelace",
ClassName = "4AHIT",
IsActive = true,
CreatedAt = now,
UpdatedAt = now
};
var activeBook = new Book
{
BookId = Guid.NewGuid(),
BookNumber = "B-1",
Title = "Active",
Author = "A",
Subject = "S",
IsActive = true,
CreatedAt = now,
UpdatedAt = now
};
var inactiveBook = new Book
{
BookId = Guid.NewGuid(),
BookNumber = "B-2",
Title = "Inactive",
Author = "A",
Subject = "S",
IsActive = false,
CreatedAt = now,
UpdatedAt = now
};
database.Context.AddRange(student, activeBook, inactiveBook, new Loan
{
LoanId = Guid.NewGuid(),
StudentId = student.StudentId,
BookId = inactiveBook.BookId,
LoanedAt = now.AddDays(-1),
DueAt = now.AddDays(13)
});
await database.Context.SaveChangesAsync();
var statistics = await new ReportService(database.Context, new FixedTimeProvider(now)).GetStatisticsAsync(CancellationToken.None);
Assert.Equal(1, statistics.TotalBooks);
Assert.Equal(1, statistics.AvailableBooks);
Assert.Equal(1, statistics.ActiveLoans);
}
[Fact]
public async Task BookList_CanFilterAvailabilityAndLimitRows()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest { CardNumber = "S-1", FirstName = "Ada", LastName = "Lovelace", ClassName = "4AHIT" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-1", Title = "Available", Author = "A", Subject = "S" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-2", Title = "Borrowed", Author = "A", Subject = "S" }, CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-2" }, CancellationToken.None);
var available = await books.GetAllAsync(availableOnly: true, skip: 0, take: 1, CancellationToken.None);
var borrowed = await books.GetAllAsync(availableOnly: false, skip: 0, take: 10, CancellationToken.None);
Assert.Single(available);
Assert.Equal("B-1", available[0].BookNumber);
Assert.Collection(borrowed, book => Assert.Equal("B-2", book.BookNumber));
}
[Fact]
public async Task Reports_ReturnsBundledReportData()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest { CardNumber = "S-1", FirstName = "Ada", LastName = "Lovelace", ClassName = "4AHIT" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-1", Title = "Overdue", Author = "A", Subject = "S" }, CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1", DueAt = clock.GetUtcNow().AddDays(1) }, CancellationToken.None);
var reports = await new ReportService(database.Context, new FixedTimeProvider(clock.GetUtcNow().AddDays(2))).GetReportsAsync(CancellationToken.None);
Assert.Single(reports.ActiveLoans);
Assert.Single(reports.OverdueLoans);
Assert.Single(reports.ActiveStudents);
Assert.Equal(1, reports.Statistics.ActiveLoans);
Assert.Equal(0, reports.Statistics.AvailableBooks);
}
[Fact]
public async Task Reports_LimitsListsButKeepsFullStatistics()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var now = new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero);
var student = new Student
{
StudentId = Guid.NewGuid(),
CardNumber = "S-1",
FirstName = "Ada",
LastName = "Lovelace",
ClassName = "4AHIT",
IsActive = true,
CreatedAt = now,
UpdatedAt = now
};
database.Context.Students.Add(student);
for (var i = 0; i < 101; i++)
{
var book = new Book
{
BookId = Guid.NewGuid(),
BookNumber = $"B-{i}",
Title = $"Book {i}",
Author = "A",
Subject = "S",
IsActive = true,
CreatedAt = now,
UpdatedAt = now
};
database.Context.Add(book);
database.Context.Loans.Add(new Loan
{
LoanId = Guid.NewGuid(),
StudentId = student.StudentId,
BookId = book.BookId,
LoanedAt = now.AddDays(-1),
DueAt = now.AddDays(13)
});
}
await database.Context.SaveChangesAsync();
var reports = await new ReportService(database.Context, new FixedTimeProvider(now)).GetReportsAsync(CancellationToken.None);
Assert.Equal(100, reports.ActiveLoans.Count);
Assert.Equal(101, reports.Statistics.ActiveLoans);
}
[Fact]
public async Task Search_FindsPartialMatches()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest { CardNumber = "S-1", FirstName = "Ada", LastName = "Lovelace", ClassName = "4AHIT" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-1", Title = "Clean Architecture", Author = "Robert Martin", Subject = "Informatik" }, CancellationToken.None);
Assert.Single(await students.SearchAsync("love", CancellationToken.None));
Assert.Single(await books.SearchAsync(title: "arch", author: null, subject: null, CancellationToken.None));
}
[Fact]
public async Task Loan_ThrowsNotFound_WhenStudentOrBookIsMissing()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => loans.VerifyAsync(new VerifyLoanRequest { CardNumber = "missing", BookNumber = "missing" }, CancellationToken.None));
await students.CreateAsync(Student("S-1"), CancellationToken.None);
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "missing" }, CancellationToken.None));
await books.CreateAsync(Book("B-1"), CancellationToken.None);
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => loans.CreateAsync(new CreateLoanRequest { CardNumber = "missing", BookNumber = "B-1" }, CancellationToken.None));
}
[Fact]
public async Task Loan_ThrowsConflict_WhenBookIsAlreadyBorrowed()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
await students.CreateAsync(Student("S-1"), CancellationToken.None);
await students.CreateAsync(Student("S-2"), CancellationToken.None);
await books.CreateAsync(Book("B-1"), CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1" }, CancellationToken.None);
await Assert.ThrowsAsync<ResourceConflictException>(
() => loans.VerifyAsync(new VerifyLoanRequest { CardNumber = "S-2", BookNumber = "B-1" }, CancellationToken.None));
await Assert.ThrowsAsync<ResourceConflictException>(
() => loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-2", BookNumber = "B-1" }, CancellationToken.None));
}
[Fact]
public async Task Loan_ThrowsValidation_WhenDueDateIsInPast()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
await students.CreateAsync(Student("S-1"), CancellationToken.None);
await books.CreateAsync(Book("B-1"), CancellationToken.None);
await Assert.ThrowsAsync<RequestValidationException>(
() => loans.CreateAsync(new CreateLoanRequest
{
CardNumber = "S-1",
BookNumber = "B-1",
DueAt = clock.GetUtcNow().AddSeconds(-1)
}, CancellationToken.None));
}
[Fact]
public async Task Return_ThrowsNotFound_WhenNoActiveLoanExists()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var returns = new ReturnService(database.Context, NewClock());
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => returns.VerifyAsync("missing", CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => returns.ReturnAsync(new ReturnBookRequest { BookNumber = "missing" }, CancellationToken.None));
}
[Fact]
public async Task Return_ReportsOverdueDays()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var start = new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
var overdueClock = new FixedTimeProvider(start.GetUtcNow().AddDays(3));
var students = new StudentService(database.Context, start);
var books = new BookService(database.Context, start);
var loans = new LoanService(database.Context, start);
await students.CreateAsync(Student("S-1"), CancellationToken.None);
await books.CreateAsync(Book("B-1"), CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest
{
CardNumber = "S-1",
BookNumber = "B-1",
DueAt = start.GetUtcNow().AddDays(1)
}, CancellationToken.None);
var returns = new ReturnService(database.Context, overdueClock);
var verification = await returns.VerifyAsync("B-1", CancellationToken.None);
var result = await returns.ReturnAsync(new ReturnBookRequest { BookNumber = "B-1" }, CancellationToken.None);
Assert.True(verification.IsOverdue);
Assert.Equal(2, verification.DaysOverdue);
Assert.True(result.WasOverdue);
Assert.Equal(2, result.DaysOverdue);
}
[Fact]
public async Task Delete_ThrowsConflict_WhenActiveLoanExists()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
var student = await students.CreateAsync(Student("S-1"), CancellationToken.None);
var book = await books.CreateAsync(Book("B-1"), CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1" }, CancellationToken.None);
await Assert.ThrowsAsync<ResourceConflictException>(() => students.DeleteAsync(student.Id, CancellationToken.None));
await Assert.ThrowsAsync<ResourceConflictException>(() => books.DeleteAsync(book.Id, CancellationToken.None));
}
[Fact]
public async Task Delete_DeactivatesStudentAndBook()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var student = await students.CreateAsync(Student("S-1"), CancellationToken.None);
var book = await books.CreateAsync(Book("B-1"), CancellationToken.None);
await students.DeleteAsync(student.Id, CancellationToken.None);
await books.DeleteAsync(book.Id, CancellationToken.None);
Assert.Empty(await students.GetAllAsync(0, 10, CancellationToken.None));
Assert.Empty(await books.GetAllAsync(null, 0, 10, CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(() => students.GetByIdAsync(student.Id, CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(() => books.GetByIdAsync(book.Id, CancellationToken.None));
}
[Fact]
public async Task BookIdentifiers_AreUnique()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var books = new BookService(database.Context, NewClock());
await books.CreateAsync(Book("B-1", isbn: "978-1"), CancellationToken.None);
await Assert.ThrowsAsync<ResourceConflictException>(
() => books.CreateAsync(Book("b-1"), CancellationToken.None));
await Assert.ThrowsAsync<ResourceConflictException>(
() => books.CreateAsync(Book("B-2", isbn: "978-1"), CancellationToken.None));
}
[Fact]
public async Task Update_ThrowsConflict_WhenIdentifiersAlreadyExist()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
await students.CreateAsync(Student("S-1"), CancellationToken.None);
var secondStudent = await students.CreateAsync(Student("S-2"), CancellationToken.None);
await books.CreateAsync(Book("B-1", isbn: "978-1"), CancellationToken.None);
var secondBook = await books.CreateAsync(Book("B-2", isbn: "978-2"), CancellationToken.None);
await Assert.ThrowsAsync<ResourceConflictException>(
() => students.UpdateAsync(secondStudent.Id, Student("S-1"), CancellationToken.None));
await Assert.ThrowsAsync<ResourceConflictException>(
() => books.UpdateAsync(secondBook.Id, Book("B-1", isbn: "978-2"), CancellationToken.None));
await Assert.ThrowsAsync<ResourceConflictException>(
() => books.UpdateAsync(secondBook.Id, Book("B-2", isbn: "978-1"), CancellationToken.None));
}
[Fact]
public async Task Search_ThrowsValidation_WhenSearchInputIsMissing()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
await Assert.ThrowsAsync<RequestValidationException>(
() => students.SearchAsync(" ", CancellationToken.None));
await Assert.ThrowsAsync<RequestValidationException>(
() => books.SearchAsync(null, null, null, CancellationToken.None));
}
[Fact]
public async Task Reads_ThrowNotFound_WhenResourcesDoNotExist()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => students.GetByIdAsync(Guid.NewGuid(), CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => students.GetByCardAsync("missing", CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => books.GetByIdAsync(Guid.NewGuid(), CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => books.GetByNumberAsync("missing", CancellationToken.None));
}
[Fact]
public async Task Paging_ClampsInvalidSkipAndTake()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
await students.CreateAsync(Student("S-1"), CancellationToken.None);
await books.CreateAsync(Book("B-1"), CancellationToken.None);
Assert.Single(await students.GetAllAsync(-10, 0, CancellationToken.None));
Assert.Single(await books.GetAllAsync(null, -10, 0, CancellationToken.None));
}
[Fact]
public async Task Update_ChangesStudentAndBookValues()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var student = await students.CreateAsync(Student("S-1"), CancellationToken.None);
var book = await books.CreateAsync(Book("B-1"), CancellationToken.None);
var updatedStudent = await students.UpdateAsync(student.Id, new SaveStudentRequest
{
CardNumber = "S-2",
FirstName = "Grace",
LastName = "Hopper",
ClassName = "5BHIT",
Email = "grace@example.test"
}, CancellationToken.None);
var updatedBook = await books.UpdateAsync(book.Id, new SaveBookRequest
{
BookNumber = "B-2",
Isbn = "978-2",
Title = "Compiler Design",
Author = "Grace Hopper",
Subject = "Informatik",
Publisher = "ACM",
PublicationYear = 1952
}, CancellationToken.None);
Assert.Equal("Grace Hopper", updatedStudent.FullName);
Assert.Equal("S-2", updatedStudent.CardNumber);
Assert.Equal("grace@example.test", updatedStudent.Email);
Assert.Equal("Compiler Design", updatedBook.Title);
Assert.Equal("978-2", updatedBook.Isbn);
Assert.Equal(1952, updatedBook.PublicationYear);
}
[Fact]
public async Task Search_CoversStudentFullNameAndBookAuthorAndSubject()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest { CardNumber = "S-1", FirstName = "Ada", LastName = "Lovelace", ClassName = "4AHIT" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-1", Title = "Clean Architecture", Author = "Robert Martin", Subject = "Software Engineering" }, CancellationToken.None);
Assert.Single(await students.SearchAsync("ada love", CancellationToken.None));
Assert.Single(await books.SearchAsync(title: null, author: "martin", subject: null, CancellationToken.None));
Assert.Single(await books.SearchAsync(title: null, author: null, subject: "engineering", CancellationToken.None));
Assert.Empty(await books.SearchAsync(title: "missing", author: null, subject: null, CancellationToken.None));
}
[Fact]
public async Task Lists_AreSortedAndPaged()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
var reports = new ReportService(database.Context, clock);
await students.CreateAsync(new SaveStudentRequest { CardNumber = "S-1", FirstName = "B", LastName = "Beta", ClassName = "1A" }, CancellationToken.None);
await students.CreateAsync(new SaveStudentRequest { CardNumber = "S-2", FirstName = "A", LastName = "Alpha", ClassName = "1A" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-1", Title = "Zulu", Author = "A", Subject = "S" }, CancellationToken.None);
await books.CreateAsync(new SaveBookRequest { BookNumber = "B-2", Title = "Alpha", Author = "A", Subject = "S" }, CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1", DueAt = clock.GetUtcNow().AddDays(5) }, CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-2", BookNumber = "B-2", DueAt = clock.GetUtcNow().AddDays(1) }, CancellationToken.None);
var orderedStudents = await students.GetAllAsync(0, 10, CancellationToken.None);
var pagedBooks = await books.GetAllAsync(null, 1, 1, CancellationToken.None);
var orderedLoans = await reports.GetActiveLoansAsync(0, 10, CancellationToken.None);
Assert.Equal(["Alpha", "Beta"], orderedStudents.Select(student => student.LastName).ToArray());
Assert.Equal("Zulu", pagedBooks.Single().Title);
Assert.Equal(["Alpha", "Zulu"], orderedLoans.Select(loan => loan.BookTitle).ToArray());
}
[Fact]
public async Task Return_ThrowsNotFound_WhenBookIsReturnedTwice()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var loans = new LoanService(database.Context, clock);
var returns = new ReturnService(database.Context, clock);
await students.CreateAsync(Student("S-1"), CancellationToken.None);
await books.CreateAsync(Book("B-1"), CancellationToken.None);
await loans.CreateAsync(new CreateLoanRequest { CardNumber = "S-1", BookNumber = "B-1" }, CancellationToken.None);
await returns.ReturnAsync(new ReturnBookRequest { BookNumber = "B-1" }, CancellationToken.None);
await Assert.ThrowsAsync<ResourceNotFoundException>(
() => returns.ReturnAsync(new ReturnBookRequest { BookNumber = "B-1" }, CancellationToken.None));
}
[Fact]
public async Task SoftDeletedResources_AreHiddenFromSearchAndLookup()
{
await using var database = new TestDatabase();
await database.InitializeAsync();
var clock = NewClock();
var students = new StudentService(database.Context, clock);
var books = new BookService(database.Context, clock);
var student = await students.CreateAsync(Student("S-1"), CancellationToken.None);
var book = await books.CreateAsync(Book("B-1"), CancellationToken.None);
await students.DeleteAsync(student.Id, CancellationToken.None);
await books.DeleteAsync(book.Id, CancellationToken.None);
Assert.Empty(await students.SearchAsync("Ada", CancellationToken.None));
Assert.Empty(await books.SearchAsync(title: "Book", author: null, subject: null, CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(() => students.GetByCardAsync("S-1", CancellationToken.None));
await Assert.ThrowsAsync<ResourceNotFoundException>(() => books.GetByNumberAsync("B-1", CancellationToken.None));
}
private static FixedTimeProvider NewClock() =>
new(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero));
private static SaveStudentRequest Student(string cardNumber) =>
new()
{
CardNumber = cardNumber,
FirstName = "Ada",
LastName = "Lovelace",
ClassName = "4AHIT"
};
private static SaveBookRequest Book(string bookNumber, string? isbn = null) =>
new()
{
BookNumber = bookNumber,
Isbn = isbn,
Title = $"Book {bookNumber}",
Author = "Author",
Subject = "Subject"
};
}