From fb9971a32c325d9195c4e58649bbc005ebf7b886 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 10 Nov 2025 12:30:23 +0100 Subject: [PATCH] feat: bug fixes, basic set of tests --- Database.cs | 3 +- Knihovna.csproj | 1 + Program.cs | 48 +--- docs/test.md | 89 ++++++++ src/Tests.cs | 572 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 675 insertions(+), 38 deletions(-) create mode 100644 src/Tests.cs diff --git a/Database.cs b/Database.cs index e9a4509..806c68e 100644 --- a/Database.cs +++ b/Database.cs @@ -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(); } diff --git a/Knihovna.csproj b/Knihovna.csproj index 7385a14..52f2e5b 100644 --- a/Knihovna.csproj +++ b/Knihovna.csproj @@ -8,6 +8,7 @@ + diff --git a/Program.cs b/Program.cs index 9aa4daa..01b1d88 100644 --- a/Program.cs +++ b/Program.cs @@ -9,41 +9,15 @@ using Team4; // Team 4 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); - - - - } - - } - } - catch (SqliteException ex) - { - Console.WriteLine($"{ex.Source}: {ex.Message}"); - } - } + public static void Main() + { + try + { + Tests.TestRunner.RunAll(); + } + catch (SqliteException ex) + { + Console.WriteLine($"{ex.Source}: {ex.Message}"); + } + } } diff --git a/docs/test.md b/docs/test.md index f667a94..2358d5c 100644 --- a/docs/test.md +++ b/docs/test.md @@ -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. diff --git a/src/Tests.cs b/src/Tests.cs new file mode 100644 index 0000000..40824db --- /dev/null +++ b/src/Tests.cs @@ -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 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 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 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 stolenAll = Team4.Team4.GetStolenBooks(conn); + if (stolenAll == null || stolenAll.Count == 0) throw new Exception("Expected stolen books to include the overdue borrow"); + List 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 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 _ = 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"); + } + } + } +} +