feat: bug fixes, basic set of tests

This commit is contained in:
Frank 2025-11-10 12:30:23 +01:00
parent 4a7491756a
commit fb9971a32c
5 changed files with 675 additions and 38 deletions

View File

@ -13,6 +13,7 @@ namespace Database
[Name] TEXT NOT NULL,
[YearOfRelease] INTEGER,
[Total] INTEGER,
[Available] INTEGER,
[AuthorID] INTEGER,
[PublisherID] INTEGER,
FOREIGN KEY ([AuthorID]) REFERENCES [Authors]([ID]),
@ -44,7 +45,7 @@ namespace Database
[Name] TEXT,
[Surname] TEXT
);";
var command = conn.CreateCommand();
SqliteCommand command = conn.CreateCommand();
command.CommandText = createTableQuery;
command.ExecuteNonQuery();
}

View File

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Normalizace/**/*.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -11,35 +11,9 @@ public class Program
{
public static void Main()
{
string db = "Database.sqlite";
string connectionString = "Data Source=" + db;
try
{
using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
try
{
Database.Database.CreateDatabaseQuery(connection);
}
catch
{
Console.WriteLine("Database already exists.");
}
using (var command = connection.CreateCommand())
{
// implment stuff here idc
AuthorDto author = Team2.Team2.AddAuthor(connection, "John", "Doe", new DateTime(1990, 1, 1));
AuthorDto getAuthor = Team4.Team4.GetAuthorById(connection, author.Id);
}
}
Tests.TestRunner.RunAll();
}
catch (SqliteException ex)
{

View File

@ -16,3 +16,92 @@ Prazdne inputy od uzivatele
Consecutive r/w
Type checking
Fuzzing inputu
-----------------------------------------
Scénáře (přidání knihy) + očekávané výstupy
- Předpoklady testů:
- Existuje `AuthorID` a `PublisherID` v DB (jinak přidání knihy selže a funkce vrátí null).
- Implementace `Team2.AddBook` vkládá přes `INSERT OR IGNORE` a následně dělá `SELECT ... LIMIT 1` podle kombinace `(Name, AuthorID, PublisherID, YearOfRelease)`.
- Poznámka ke schématu: pokud tabulka `Books` NEOBSAHUJE sloupec `Available`, vkládání selže SQL chybou a funkce vrátí null. Pokud `Available` existuje (v předem připravené DB), testy s `Available` dávají smysl.
- Základní flow úspěch:
- Vstup: validní `name`, existující `authorId`, existující `publisherId`, rozumný `year`, `total >= 0`, `available >= 0`.
- Očekávaný výstup: `BookDto` s nenulovým `Id`, hodnotami dle vstupu; návrat nenull.
- Neexistující autor:
- Vstup: `authorId` neexistuje v `Authors`.
- Očekávaný výstup: nenastane insert, funkce vrátí `null`.
- Neexistující publisher:
- Vstup: `publisherId` neexistuje v `Publisher`.
- Očekávaný výstup: nenastane insert, funkce vrátí `null`.
- Duplicita knihy (stejný `name` + `authorId` + `publisherId` + `year`):
- Pokud v DB existuje unikátní omezení na tuto kombinaci: `INSERT OR IGNORE` neprovede insert a `SELECT ... LIMIT 1` vrátí existující řádek ⇒ `BookDto` (nenull), nezdvojené data.
- Pokud v DB NEexistuje žádné unikátní omezení (výchozí schema v `Database.cs` ho nemá): dojde ke vložení DUPLIKÁTNÍHO řádku, `SELECT ... LIMIT 1` vrátí libovolný první shodný záznam ⇒ `BookDto` (nenull), ale data jsou duplicitní v tabulce.
- `name` prázdný řetězec:
- `Books.Name` je `NOT NULL`, ale prázdný řetězec NENÍ `NULL` ⇒ vložení projde.
- Očekávaný výstup: `BookDto` (nenull).
- `name` = null:
- Při předání `null` jako parametru může dojít k chybě binderu parametrů nebo k pokusu vložit `NULL` do `NOT NULL` sloupce.
- Očekávaný výstup: SQL nebo parametrická výjimka uvnitř metody ⇒ metoda chytá výjimku a vrátí `null`.
- `year` záporný nebo 0:
- Není validováno na aplikační úrovni ani ve schématu.
- Očekávaný výstup: vložení projde ⇒ `BookDto` (nenull).
- `total` záporné číslo:
- Není validováno; ve schématu žádné omezení.
- Očekávaný výstup: vložení projde ⇒ `BookDto` (nenull). Pozn.: následné reporty mohou dávat nelogické výsledky.
- `available` > `total`:
- Není validováno; pokud sloupec `Available` v DB existuje, vloží se bez kontroly.
- Očekávaný výstup: `BookDto` (nenull).
- `available` záporné:
- Není validováno; pokud sloupec existuje, vloží se.
- Očekávaný výstup: `BookDto` (nenull).
- Extrémně dlouhé `name`:
- Bez omezení délky; SQLite uloží. Může mít dopad na výkon a čitelnost.
- Očekávaný výstup: `BookDto` (nenull).
- Speciální znaky / SQL injection v `name`:
- Parametrizované dotazy ⇒ bezpečné vůči SQLi.
- Očekávaný výstup: `BookDto` (nenull).
- Současné (konkurenční) vkládání stejné knihy:
- Bez unikátního indexu hrozí race condition ⇒ vícenásobné duplicity.
- Očekávaný výstup: více identických řádků v `Books`.
- Nesoulad schématu: chybějící `Available` ve `Books`:
- `INSERT` obsahuje `Available` sloupec; pokud DB nemá `Available`, dojde k SQL chybě.
- Očekávaný výstup: metoda vrátí `null`.
-----------------------------------------
Scénáře (autor) “stejný autor včetně DOB”
- Vytvoření autora se stejným `Name` + `Surname` + `DateOfBirth` opakovaně:
- Pokud existuje unikátní omezení nad (Name, Surname, DateOfBirth): `INSERT OR IGNORE` neprovede insert ⇒ vrácen bude existující autor (nenull).
- Pokud unikát neexistuje (výchozí schema v `Database.cs` jej nemá): vloží se DUPLIKÁT; následný `SELECT ... LIMIT 1` vrátí některého z nich ⇒ `AuthorDto` (nenull), ale v DB jsou duplicity.
- Přidání knihy k “stejnému autorovi”:
- Logika knihy pracuje výhradně s `authorId`. Pokud existují 2 různé řádky autora se stejnými údaji (Name/Surname/DOB), ale rozdílným `ID`, přidání knihy proběhne ke kterékoliv instanci podle zvoleného `authorId`.
- Očekávaný výstup: `BookDto` (nenull), navázáno na konkrétní `AuthorID`, nikoli na shodu jména/DOB.
-----------------------------------------
Edge cases (souhrn)
- Chybějící cizí klíče (`AuthorID`, `PublisherID`) ⇒ návrat `null`.
- Duplicity bez unikátních indexů ⇒ v DB vznikají vícenásobné identické záznamy.
- Schéma vs. kód: `Available` ve `Books` pokud chybí, přidání knihy vždy selže ⇒ `null`.
- `Name` prázdný vs `NULL`: prázdný projde, `NULL` vyvolá chybu kvůli `NOT NULL`.
- Hodnoty mimo rozsah (záporné `year`/`total`/`available`) nejsou validovány.
- Neprobíhá automatické snižování zásob po výpůjčce (`Borrows` neupravují `Books.Total/Available`).
- Potenciálně nekonzistentní pluralita tabulek v jiných částech (např. `Publishers` vs `Publisher`) neovlivňuje `Team2.AddBook`, ale je rizikem v integračních testech.

572
src/Tests.cs Normal file
View File

@ -0,0 +1,572 @@
using System;
using System.Collections.Generic;
using Microsoft.Data.Sqlite;
using Database.Dto;
using Team2;
using Team3;
using Team4;
namespace Tests
{
public static class TestRunner
{
private static int _passed;
private static int _failed;
public static void RunAll()
{
_passed = 0;
_failed = 0;
Run(Test_AddBook_Success);
Run(Test_AddBook_MissingAuthor_Fails);
Run(Test_AddBook_MissingPublisher_Fails);
Run(Test_AddBook_Duplicate_AllowsWithoutUniqueIndex);
Run(Test_AddBook_EmptyName_Succeeds);
Run(Test_AddBook_NullName_Fails);
Run(Test_AddBook_NegativeValues_Succeeds);
Run(Test_AddBook_AvailableGreaterThanTotal_Succeeds);
Run(Test_Author_DuplicateSameDOB_AllowedWithoutUniqueIndex);
Run(Test_AddAuthor_EmptyName_Succeeds_And_NullName_Fails);
Run(Test_AddPublisher_Success_Duplicates_And_NullName_Fails);
Run(Test_AddUser_Success_And_DuplicatesAllowed);
Run(Test_Borrow_Success);
Run(Test_Borrow_DueDate_Default_30Days);
Run(Test_Borrow_MissingUser_Fails);
Run(Test_Borrow_MissingBook_Fails);
Run(Test_GetBooksByName_MultipleMatches);
Run(Test_GetBooksByAuthor_ReturnsBooks);
Run(Test_GetBooksByAvailableBooks_Filter);
Run(Test_StolenBooks_Scenarios);
Run(Test_StolenBooks_None_ReturnsEmpty);
Run(Test_GetAuthorById_Success_And_NotFound);
Run(Test_Team4_Methods_Throw_On_ClosedConnection);
Run(Test_Team4_All_Methods_Throw_On_ClosedConnection);
Run(Test_Team4_NotImplemented_Methods_Throw);
Run(Test_EditAuthor_Success_And_NotFound);
Run(Test_EditPublisher_Success_And_NotFound);
Run(Test_EditUser_Success_And_NotFound);
Run(Test_EditBook_Success_And_Failures);
Console.WriteLine();
Console.WriteLine($"Tests finished. Passed: {_passed}, Failed: {_failed}");
}
private static void Run(Action test)
{
string name = test.Method.Name;
try
{
test();
_passed++;
Console.WriteLine($"[PASS] {name}");
}
catch (Exception ex)
{
_failed++;
Console.WriteLine($"[FAIL] {name}: {ex.Message}");
}
}
private static SqliteConnection NewMemoryConnection()
{
SqliteConnection conn = new SqliteConnection("Data Source=:memory:");
conn.Open();
Database.Database.CreateDatabaseQuery(conn);
return conn;
}
private static int CountBooks(SqliteConnection conn, string name, int authorId, int publisherId, int year)
{
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = @"SELECT COUNT(*) FROM Books
WHERE Name = @name AND AuthorID = @aid AND PublisherID = @pid AND YearOfRelease = @year;";
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@aid", authorId);
cmd.Parameters.AddWithValue("@pid", publisherId);
cmd.Parameters.AddWithValue("@year", year);
return Convert.ToInt32(cmd.ExecuteScalar());
}
}
private static int CountAuthors(SqliteConnection conn, string name, string surname, DateTime dob)
{
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = @"SELECT COUNT(*) FROM Authors
WHERE Name = @name AND Surname = @surname AND DateOfBirth = @dob;";
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@surname", surname);
cmd.Parameters.AddWithValue("@dob", dob.ToString("yyyy-MM-dd"));
return Convert.ToInt32(cmd.ExecuteScalar());
}
}
private static int CountUsers(SqliteConnection conn, string name, string surname)
{
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = @"SELECT COUNT(*) FROM Users WHERE Name = @name AND Surname = @surname;";
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@surname", surname);
return Convert.ToInt32(cmd.ExecuteScalar());
}
}
private static int CountBorrowsForUserBook(SqliteConnection conn, int userId, int bookId)
{
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = @"SELECT COUNT(*) FROM Borrows WHERE UserID = @uid AND BookID = @bid;";
cmd.Parameters.AddWithValue("@uid", userId);
cmd.Parameters.AddWithValue("@bid", bookId);
return Convert.ToInt32(cmd.ExecuteScalar());
}
}
private static Publisher EnsurePublisher(SqliteConnection conn, string name = "Penguin", string state = "US")
{
Publisher p = Team2.Team2.AddPublisher(conn, name, state);
if (p == null || p.Id <= 0) throw new Exception("Publisher creation failed");
return p;
}
private static AuthorDto EnsureAuthor(SqliteConnection conn, string name = "John", string surname = "Doe", int year = 1990, int month = 1, int day = 1)
{
AuthorDto a = Team2.Team2.AddAuthor(conn, name, surname, new DateTime(year, month, day));
if (a == null || a.Id <= 0) throw new Exception("Author creation failed");
return a;
}
private static UserDto EnsureUser(SqliteConnection conn, string name = "Alice", string surname = "Smith")
{
UserDto u = Team2.Team2.AddUser(conn, name, surname);
if (u == null || u.Id <= 0) throw new Exception("User creation failed");
return u;
}
private static BookDto EnsureBook(SqliteConnection conn, string title = "Sample Book", int year = 2001, int total = 3, int available = 3)
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
BookDto book = Team2.Team2.AddBook(conn, title, author.Id, publisher.Id, year, total, available);
if (book == null || book.Id <= 0) throw new Exception("Book creation failed");
return book;
}
// Tests
// Team2 - Books
private static void Test_AddBook_Success()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
BookDto book = Team2.Team2.AddBook(conn, "Clean Code", author.Id, publisher.Id, 2008, 10, 10);
if (book == null || book.Id <= 0) throw new Exception("Expected book to be created");
if (book.Name != "Clean Code") throw new Exception("Name mismatch");
if (book.YearOfRelease != 2008) throw new Exception("Year mismatch");
if (book.Total != 10) throw new Exception("Total mismatch");
int cnt = CountBooks(conn, "Clean Code", author.Id, publisher.Id, 2008);
if (cnt != 1) throw new Exception("Expected exactly one row for the book");
}
}
private static void Test_AddBook_MissingAuthor_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
Publisher publisher = EnsurePublisher(conn);
BookDto book = Team2.Team2.AddBook(conn, "No Author Book", 99999, publisher.Id, 2020, 1, 1);
if (book != null) throw new Exception("Expected null when author does not exist");
}
}
private static void Test_AddBook_MissingPublisher_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
BookDto book = Team2.Team2.AddBook(conn, "No Publisher Book", author.Id, 99999, 2020, 1, 1);
if (book != null) throw new Exception("Expected null when publisher does not exist");
}
}
private static void Test_AddBook_Duplicate_AllowsWithoutUniqueIndex()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
BookDto b1 = Team2.Team2.AddBook(conn, "Duplicate", author.Id, publisher.Id, 2021, 2, 2);
if (b1 == null) throw new Exception("First insert failed");
BookDto b2 = Team2.Team2.AddBook(conn, "Duplicate", author.Id, publisher.Id, 2021, 2, 2);
if (b2 == null) throw new Exception("Second insert unexpectedly failed");
int cnt = CountBooks(conn, "Duplicate", author.Id, publisher.Id, 2021);
if (cnt < 2) throw new Exception("Expected duplicates without a unique index");
}
}
private static void Test_AddBook_EmptyName_Succeeds()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
BookDto book = Team2.Team2.AddBook(conn, "", author.Id, publisher.Id, 2000, 1, 1);
if (book == null) throw new Exception("Empty string name should be allowed (NOT NULL != empty)");
}
}
private static void Test_AddBook_NullName_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
#pragma warning disable CS8625, CS8604, CS8600
BookDto book = Team2.Team2.AddBook(conn, (string)(object)null, author.Id, publisher.Id, 2000, 1, 1);
#pragma warning restore CS8625, CS8604, CS8600
if (book != null) throw new Exception("Expected null when inserting NULL into NOT NULL column");
}
}
private static void Test_AddBook_NegativeValues_Succeeds()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
BookDto book = Team2.Team2.AddBook(conn, "NegativeVals", author.Id, publisher.Id, -1, -5, -2);
if (book == null) throw new Exception("Negative values should not be validated at DB level here");
if (book.YearOfRelease != -1) throw new Exception("Year not stored as provided");
if (book.Total != -5) throw new Exception("Total not stored as provided");
}
}
private static void Test_AddBook_AvailableGreaterThanTotal_Succeeds()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn);
Publisher publisher = EnsurePublisher(conn);
BookDto book = Team2.Team2.AddBook(conn, "AvailGtTotal", author.Id, publisher.Id, 2010, 1, 5);
if (book == null) throw new Exception("Available > Total currently not validated; insert should succeed");
}
}
private static void Test_Author_DuplicateSameDOB_AllowedWithoutUniqueIndex()
{
using (SqliteConnection conn = NewMemoryConnection())
{
string name = "Jane";
string surname = "Doe";
DateTime dob = new DateTime(1995, 5, 5);
AuthorDto a1 = Team2.Team2.AddAuthor(conn, name, surname, dob);
if (a1 == null) throw new Exception("First author insert failed");
AuthorDto a2 = Team2.Team2.AddAuthor(conn, name, surname, dob);
if (a2 == null) throw new Exception("Second author insert unexpectedly failed");
int cnt = CountAuthors(conn, name, surname, dob);
if (cnt < 2) throw new Exception("Expected duplicate authors without a unique index");
}
}
// Team2 - Users
private static void Test_AddAuthor_EmptyName_Succeeds_And_NullName_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
// Empty name should be allowed (NOT NULL != empty)
AuthorDto a1 = Team2.Team2.AddAuthor(conn, "", "S", new DateTime(1990, 1, 1));
if (a1 == null || a1.Id <= 0) throw new Exception("Empty author name should be allowed");
// Null name should fail and return null
#pragma warning disable CS8625, CS8604, CS8600
AuthorDto a2 = Team2.Team2.AddAuthor(conn, (string)(object)null, "S", new DateTime(1990, 1, 1));
#pragma warning restore CS8625, CS8604, CS8600
if (a2 != null) throw new Exception("Expected null when inserting NULL author name");
}
}
private static void Test_AddPublisher_Success_Duplicates_And_NullName_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
Publisher p1 = Team2.Team2.AddPublisher(conn, "PubX", "CZ");
if (p1 == null || p1.Id <= 0) throw new Exception("Publisher insert failed");
Publisher p2 = Team2.Team2.AddPublisher(conn, "PubX", "CZ");
if (p2 == null || p2.Id <= 0) throw new Exception("Duplicate publisher insert unexpectedly failed");
#pragma warning disable CS8625, CS8604, CS8600
Publisher p3 = Team2.Team2.AddPublisher(conn, (string)(object)null, "CZ");
#pragma warning restore CS8625, CS8604, CS8600
if (p3 != null) throw new Exception("Expected null when inserting NULL publisher name");
}
}
private static void Test_AddUser_Success_And_DuplicatesAllowed()
{
using (SqliteConnection conn = NewMemoryConnection())
{
UserDto u1 = Team2.Team2.AddUser(conn, "Bob", "Brown");
if (u1 == null || u1.Id <= 0) throw new Exception("User insert failed");
UserDto u2 = Team2.Team2.AddUser(conn, "Bob", "Brown");
if (u2 == null || u2.Id <= 0) throw new Exception("Duplicate user insert unexpectedly failed");
int cnt = CountUsers(conn, "Bob", "Brown");
if (cnt < 2) throw new Exception("Expected duplicate users without a unique index");
}
}
// Team2 - Borrow
private static void Test_Borrow_Success()
{
using (SqliteConnection conn = NewMemoryConnection())
{
UserDto user = EnsureUser(conn);
BookDto book = EnsureBook(conn, "Borrowed Book", 2015, 5, 5);
DateTime borrowed = DateTime.UtcNow.Date;
Borrow borrow = Team2.Team2.AddBorrowedBookRecord(conn, book.Id, user.Id, borrowed);
if (borrow == null || borrow.Id <= 0) throw new Exception("Borrow creation failed");
if (borrow.User == null || borrow.User.Id != user.Id) throw new Exception("Borrow.User mismatch");
if (borrow.Book == null || borrow.Book.Id != book.Id) throw new Exception("Borrow.Book mismatch");
int cnt = CountBorrowsForUserBook(conn, user.Id, book.Id);
if (cnt != 1) throw new Exception("Expected exactly one borrow record");
}
}
private static void Test_Borrow_DueDate_Default_30Days()
{
using (SqliteConnection conn = NewMemoryConnection())
{
UserDto user = EnsureUser(conn);
BookDto book = EnsureBook(conn, "Borrowed Book 2", 2016, 2, 2);
DateTime borrowed = new DateTime(2020, 1, 1);
Borrow borrow = Team2.Team2.AddBorrowedBookRecord(conn, book.Id, user.Id, borrowed);
if (borrow == null) throw new Exception("Borrow creation failed");
DateTime expectedDue = borrowed.AddDays(30);
if (borrow.ReturnDue.Date != expectedDue.Date) throw new Exception($"ReturnDue mismatch: got {borrow.ReturnDue.Date:yyyy-MM-dd}, expected {expectedDue:yyyy-MM-dd}");
}
}
private static void Test_Borrow_MissingUser_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
BookDto book = EnsureBook(conn);
Borrow borrow = Team2.Team2.AddBorrowedBookRecord(conn, book.Id, 999999, DateTime.UtcNow.Date);
if (borrow != null) throw new Exception("Expected null when borrowing with missing user");
}
}
private static void Test_Borrow_MissingBook_Fails()
{
using (SqliteConnection conn = NewMemoryConnection())
{
UserDto user = EnsureUser(conn);
Borrow borrow = Team2.Team2.AddBorrowedBookRecord(conn, 999999, user.Id, DateTime.UtcNow.Date);
if (borrow != null) throw new Exception("Expected null when borrowing with missing book");
}
}
// Team4 - Queries and exceptions
private static void Test_GetBooksByName_MultipleMatches()
{
using (SqliteConnection conn = NewMemoryConnection())
{
BookDto b1 = EnsureBook(conn, "SameName", 2000, 1, 1);
BookDto b2 = EnsureBook(conn, "SameName", 2001, 2, 2);
List<BookDto> books = Team4.Team4.GetBooksByName(conn, "SameName");
if (books == null || books.Count < 2) throw new Exception("Expected at least two books with same name");
}
}
private static void Test_GetBooksByAuthor_ReturnsBooks()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto author = EnsureAuthor(conn, "Auth", "One", 1980, 1, 1);
Publisher publisher = EnsurePublisher(conn, "P1", "US");
BookDto b1 = Team2.Team2.AddBook(conn, "ByAuthor1", author.Id, publisher.Id, 2010, 1, 1);
BookDto b2 = Team2.Team2.AddBook(conn, "ByAuthor2", author.Id, publisher.Id, 2011, 1, 1);
List<BookDto> books = Team4.Team4.GetBooksByAuthor(conn, author);
if (books == null || books.Count < 2) throw new Exception("Expected two books by author");
}
}
private static void Test_GetBooksByAvailableBooks_Filter()
{
using (SqliteConnection conn = NewMemoryConnection())
{
BookDto b1 = EnsureBook(conn, "Stock1", 2000, 0, 0);
BookDto b2 = EnsureBook(conn, "Stock2", 2001, 2, 2);
List<BookDto> books = Team4.Team4.GetBooksByAvailableBooks(conn, 1);
if (books == null) throw new Exception("Expected list, got null");
bool hasStock2 = false;
for (int i = 0; i < books.Count; i++)
{
if (books[i].Name == "Stock2") { hasStock2 = true; break; }
}
if (!hasStock2) throw new Exception("Expected Stock2 in results for minStock=1");
}
}
private static void Test_StolenBooks_Scenarios()
{
using (SqliteConnection conn = NewMemoryConnection())
{
UserDto user = EnsureUser(conn);
BookDto book = EnsureBook(conn, "Overdue", 2005, 1, 1);
// Borrow far in the past so ReturnDue < now
DateTime borrowedPast = DateTime.UtcNow.Date.AddDays(-60);
Borrow br = Team2.Team2.AddBorrowedBookRecord(conn, book.Id, user.Id, borrowedPast);
if (br == null) throw new Exception("Failed to create overdue borrow");
List<BookDto> stolenAll = Team4.Team4.GetStolenBooks(conn);
if (stolenAll == null || stolenAll.Count == 0) throw new Exception("Expected stolen books to include the overdue borrow");
List<BookDto> stolenByUser = Team4.Team4.GetStolenBooksByUser(conn, user);
if (stolenByUser == null || stolenByUser.Count == 0) throw new Exception("Expected stolen books by user");
}
}
private static void Test_StolenBooks_None_ReturnsEmpty()
{
using (SqliteConnection conn = NewMemoryConnection())
{
List<BookDto> stolenAll = Team4.Team4.GetStolenBooks(conn);
if (stolenAll == null) throw new Exception("Expected empty list, got null");
if (stolenAll.Count != 0) throw new Exception("Expected no stolen books initially");
}
}
private static void Test_GetAuthorById_Success_And_NotFound()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto a = EnsureAuthor(conn, "Lookup", "Author", 1970, 1, 1);
AuthorDto ok = Team4.Team4.GetAuthorById(conn, a.Id);
if (ok == null || ok.Id != a.Id) throw new Exception("Expected author lookup success");
AuthorDto missing = Team4.Team4.GetAuthorById(conn, 999999);
if (missing != null) throw new Exception("Expected null for missing author");
}
}
private static void Test_Team4_Methods_Throw_On_ClosedConnection()
{
SqliteConnection conn = new SqliteConnection("Data Source=:memory:");
conn.Open();
Database.Database.CreateDatabaseQuery(conn);
conn.Close();
bool threw = false;
try
{
List<BookDto> _ = Team4.Team4.GetBooksByName(conn, "Anything");
}
catch (Exception)
{
threw = true;
}
if (!threw) throw new Exception("Expected Team4.GetBooksByName to throw on closed connection");
}
private static void Test_Team4_All_Methods_Throw_On_ClosedConnection()
{
SqliteConnection conn = new SqliteConnection("Data Source=:memory:");
conn.Open();
Database.Database.CreateDatabaseQuery(conn);
conn.Close();
string fail(string method) { return $"Expected {method} to throw on closed connection"; }
try { Team4.Team4.GetBooksByAuthor(conn, new AuthorDto { Id = 1, Name = "", Surname = "", DateOfBirth = new DateTime(2000, 1, 1) }); throw new Exception(fail("GetBooksByAuthor")); } catch (Exception) { }
try { Team4.Team4.GetBooksByAvailableBooks(conn, 1); throw new Exception(fail("GetBooksByAvailableBooks")); } catch (Exception) { }
try { Team4.Team4.GetStolenBooks(conn); throw new Exception(fail("GetStolenBooks")); } catch (Exception) { }
try { Team4.Team4.GetStolenBooksByUser(conn, new UserDto { Id = 1, Name = "", Surname = "" }); throw new Exception(fail("GetStolenBooksByUser")); } catch (Exception) { }
try { Team4.Team4.GetAuthorById(conn, 1); throw new Exception(fail("GetAuthorById")); } catch (Exception) { }
}
private static void Test_Team4_NotImplemented_Methods_Throw()
{
using (SqliteConnection conn = NewMemoryConnection())
{
bool threw1 = false;
try { Team4.Team4.GetBooksBy(conn); } catch (NotImplementedException) { threw1 = true; }
if (!threw1) throw new Exception("Expected NotImplementedException from GetBooksBy");
bool threw2 = false;
try { Team4.Team4.GetBorrowLogBy(conn); } catch (NotImplementedException) { threw2 = true; }
if (!threw2) throw new Exception("Expected NotImplementedException from GetBorrowLogBy");
}
}
// Team3 - Edit operations
private static void Test_EditAuthor_Success_And_NotFound()
{
using (SqliteConnection conn = NewMemoryConnection())
{
AuthorDto a = EnsureAuthor(conn, "Old", "Name", 1985, 2, 2);
a.Name = "New";
a.Surname = "Surname";
AuthorDto? updated = EditRecords.EditAuthor(conn, a);
if (updated == null || updated.Name != "New" || updated.Surname != "Surname") throw new Exception("EditAuthor failed to update");
AuthorDto? notFound = EditRecords.EditAuthor(conn, new AuthorDto { Id = 999999, Name = "X", Surname = "Y", DateOfBirth = new DateTime(1990, 1, 1) });
if (notFound != null) throw new Exception("Expected null for non-existing author");
}
}
private static void Test_EditPublisher_Success_And_NotFound()
{
using (SqliteConnection conn = NewMemoryConnection())
{
Publisher p = EnsurePublisher(conn, "OldPub", "UK");
p.Name = "NewPub";
p.State = "CZ";
Publisher? updated = EditRecords.EditPublisher(conn, p);
if (updated == null || updated.Name != "NewPub" || updated.State != "CZ") throw new Exception("EditPublisher failed to update");
Publisher? notFound = EditRecords.EditPublisher(conn, new Publisher { Id = 999999, Name = "X", State = "Y" });
if (notFound != null) throw new Exception("Expected null for non-existing publisher");
}
}
private static void Test_EditUser_Success_And_NotFound()
{
using (SqliteConnection conn = NewMemoryConnection())
{
UserDto u = EnsureUser(conn, "Carl", "Jr");
u.Name = "Carlos";
u.Surname = "Senior";
UserDto? updated = EditRecords.EditUser(conn, u);
if (updated == null || updated.Name != "Carlos" || updated.Surname != "Senior") throw new Exception("EditUser failed to update");
UserDto? notFound = EditRecords.EditUser(conn, new UserDto { Id = 999999, Name = "X", Surname = "Y" });
if (notFound != null) throw new Exception("Expected null for non-existing user");
}
}
private static void Test_EditBook_Success_And_Failures()
{
using (SqliteConnection conn = NewMemoryConnection())
{
// Seed original
AuthorDto a1 = EnsureAuthor(conn, "A", "One", 1970, 1, 1);
Publisher p1 = EnsurePublisher(conn, "Pub1", "US");
BookDto b = Team2.Team2.AddBook(conn, "Original", a1.Id, p1.Id, 1999, 1, 1);
if (b == null) throw new Exception("Seeding book failed");
// Prepare updates
AuthorDto a2 = EnsureAuthor(conn, "A", "Two", 1975, 1, 1);
Publisher p2 = EnsurePublisher(conn, "Pub2", "DE");
b.Name = "Updated";
b.Author = a2;
b.Publisher = p2;
b.YearOfRelease = 2000;
b.Total = 5;
BookDto? updated = EditRecords.EditBook(conn, b);
if (updated == null || updated.Name != "Updated" || updated.Author.Id != a2.Id || updated.Publisher.Id != p2.Id) throw new Exception("EditBook failed to update");
// Not found
BookDto? notFound = EditRecords.EditBook(conn, new BookDto { Id = 999999, Name = "X", YearOfRelease = 2000, Total = 1, Author = a2, Publisher = p2 });
if (notFound != null) throw new Exception("Expected null for non-existing book");
// Invalid author
BookDto invalidAuthor = new BookDto { Id = b.Id, Name = "X", YearOfRelease = 2001, Total = 1, Author = new AuthorDto { Id = 999999, Name = "Na", Surname = "Na", DateOfBirth = new DateTime(1970, 1, 1) }, Publisher = p2 };
if (EditRecords.EditBook(conn, invalidAuthor) != null) throw new Exception("Expected null when editing with non-existing author");
// Invalid publisher
BookDto invalidPublisher = new BookDto { Id = b.Id, Name = "X", YearOfRelease = 2001, Total = 1, Author = a2, Publisher = new Publisher { Id = 999999, Name = "Na", State = "Na" } };
if (EditRecords.EditBook(conn, invalidPublisher) != null) throw new Exception("Expected null when editing with non-existing publisher");
}
}
}
}