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");
+ }
+ }
+ }
+}
+