diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/DonationSelect.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/DonationSelect.java index abcca9a..075e0d0 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/DonationSelect.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/DonationSelect.java @@ -9,13 +9,41 @@ import ntnu.systemutvikling.team6.models.Donation; import ntnu.systemutvikling.team6.models.DonationRegistry; +/** + * Data access class responsible for reading donation data from the database. + * + *
Retrieves donations along with their associated charity information by + * performing an INNER JOIN between the {@code Donations} and {@code Charities} tables. + * Only donations that have a matching charity record are returned.
+ */ + public class DonationSelect { + + /** The database connection used for all queries in this class. */ private final DatabaseConnection connection; + /** + * Constructs a new {@code DonationSelect} with the given database connection. + * + * @param connection the {@link DatabaseConnection} to use for executing queries; + * must not be {@code null} + */ public DonationSelect(DatabaseConnection connection) { this.connection = connection; } + /** + * Retrieves all donations from the database, each populated with its associated + * {@link Charity}. + * + *The query performs an INNER JOIN between the {@code Donations} and + * {@code Charities} tables on the charity UUID foreign key. Donations without a + * matching charity are excluded from the result.
+ * + * @return a {@link DonationRegistry} containing all matched donations; + * never {@code null}, but may be empty if no rows are returned + * @throws RuntimeException if a {@link SQLException} occurs while executing the query + */ public DonationRegistry getDonationFromDB() { DonationRegistry registry = null; Connection conn = null; diff --git a/helpmehelpapplication/src/test/java/ntnu/systemutvikling/team6/database/Readers/DonationSelectTest.java b/helpmehelpapplication/src/test/java/ntnu/systemutvikling/team6/database/Readers/DonationSelectTest.java new file mode 100644 index 0000000..f7d8b36 --- /dev/null +++ b/helpmehelpapplication/src/test/java/ntnu/systemutvikling/team6/database/Readers/DonationSelectTest.java @@ -0,0 +1,218 @@ +package ntnu.systemutvikling.team6.database.Readers; + +import ntnu.systemutvikling.team6.database.DatabaseConnection; +import ntnu.systemutvikling.team6.models.Donation; +import ntnu.systemutvikling.team6.models.DonationRegistry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.*; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link DonationSelect}. + * + *Uses Mockito to mock the entire JDBC stack so no real database + * connection is required.
+ */ +@ExtendWith(MockitoExtension.class) +class DonationSelectTest { + + @Mock private DatabaseConnection mockDatabaseConnection; + @Mock private Connection mockConnection; + @Mock private Statement mockStatement; + @Mock private ResultSet mockResultSet; + @Mock private Date mockSqlDate; + + private DonationSelect donationSelect; + + @BeforeEach + void setUp() { + donationSelect = new DonationSelect(mockDatabaseConnection); + } + + // ------------------------------------------------------------------------- + // getDonationFromDB + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getDonationFromDB – empty result set returns an empty registry") + void getDonationFromDB_emptyResultSet_returnsEmptyRegistry() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + when(mockResultSet.next()).thenReturn(false); + + DonationRegistry registry = donationSelect.getDonationFromDB(); + + assertNotNull(registry); + assertTrue(registry.getDonations().isEmpty(), + "Registry should be empty when the result set has no rows"); + } + + @Test + @DisplayName("getDonationFromDB – single row returns one Donation with correct data") + void getDonationFromDB_singleRow_returnsSingleDonation() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, false); + stubCharityColumns("charity-uuid-1", "123456789", "Test Charity", + "https://example.org", true, "ACTIVE"); + stubDonationColumns("donation-uuid-1", 250.0, LocalDate.of(2024, 5, 20)); + + DonationRegistry registry = donationSelect.getDonationFromDB(); + + assertEquals(1, registry.getDonations().size()); + Donation donation = registry.getDonations().get(0); + assertEquals("donation-uuid-1", donation.getUUID()); + assertEquals(250.0, donation.getAmount()); + assertEquals(LocalDate.of(2024, 5, 20), donation.getDate()); + } + + @Test + @DisplayName("getDonationFromDB – single row maps charity fields onto the Donation correctly") + void getDonationFromDB_singleRow_charityMappedCorrectly() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, false); + stubCharityColumns("charity-uuid-1", "987654321", "Help Fund", + "https://helpfund.org", false, "PENDING"); + stubDonationColumns("donation-uuid-1", 100.0, LocalDate.of(2024, 1, 1)); + + DonationRegistry registry = donationSelect.getDonationFromDB(); + + Donation donation = registry.getDonations().get(0); + assertEquals("charity-uuid-1", donation.getCharity().getUUID()); + assertEquals("987654321", donation.getCharity().getOrgNumber()); + assertEquals("Help Fund", donation.getCharity().getCharityName()); + assertEquals("https://helpfund.org", donation.getCharity().getCharityLink()); + assertFalse(donation.getCharity().isPreApproved()); + assertEquals("PENDING", donation.getCharity().getStatus()); + } + + @Test + @DisplayName("getDonationFromDB – two rows returns two Donation objects") + void getDonationFromDB_twoRows_returnsTwoDonations() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, true, false); + + when(mockResultSet.getString("UUID_charities")).thenReturn("charity-uuid-1", "charity-uuid-2"); + when(mockResultSet.getString("org_number")).thenReturn("111111111", "222222222"); + when(mockResultSet.getString("charity_name")).thenReturn("Charity A", "Charity B"); + when(mockResultSet.getString("charity_link")).thenReturn("https://a.org", "https://b.org"); + when(mockResultSet.getBoolean("pre_approved")).thenReturn(true, false); + when(mockResultSet.getString("status")).thenReturn("ACTIVE", "INACTIVE"); + + when(mockResultSet.getString("UUID_Donations")).thenReturn("donation-uuid-1", "donation-uuid-2"); + when(mockResultSet.getDouble("amount")).thenReturn(500.0, 750.0); + + Date sqlDate = Date.valueOf(LocalDate.of(2024, 8, 10)); + when(mockResultSet.getDate("date")).thenReturn(sqlDate); + + DonationRegistry registry = donationSelect.getDonationFromDB(); + + assertEquals(2, registry.getDonations().size(), + "Registry should contain two donations for two result rows"); + } + + @Test + @DisplayName("getDonationFromDB – donation amount of zero is stored correctly") + void getDonationFromDB_zeroAmount_storedCorrectly() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, false); + stubCharityColumns("charity-uuid-1", "123456789", "Test Charity", + "https://example.org", true, "ACTIVE"); + stubDonationColumns("donation-uuid-zero", 0.0, LocalDate.of(2024, 1, 1)); + + DonationRegistry registry = donationSelect.getDonationFromDB(); + + assertEquals(0.0, registry.getDonations().get(0).getAmount()); + } + + @Test + @DisplayName("getDonationFromDB – large donation amount is stored correctly") + void getDonationFromDB_largeAmount_storedCorrectly() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, false); + stubCharityColumns("charity-uuid-1", "123456789", "Test Charity", + "https://example.org", true, "ACTIVE"); + stubDonationColumns("donation-uuid-big", 1_000_000.99, LocalDate.of(2024, 12, 31)); + + DonationRegistry registry = donationSelect.getDonationFromDB(); + + assertEquals(1_000_000.99, registry.getDonations().get(0).getAmount(), 0.001); + } + + @Test + @DisplayName("getDonationFromDB – SQLException is wrapped in RuntimeException") + void getDonationFromDB_sqlException_throwsRuntimeException() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenThrow(new SQLException("DB error")); + + assertThrows(RuntimeException.class, () -> donationSelect.getDonationFromDB(), + "A SQLException should be rethrown as a RuntimeException"); + } + + @Test + @DisplayName("getDonationFromDB – RuntimeException message contains expected error text") + void getDonationFromDB_sqlException_runtimeExceptionHasExpectedMessage() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenThrow(new SQLException("DB error")); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> donationSelect.getDonationFromDB()); + assertTrue(ex.getMessage().contains("ERROR"), + "RuntimeException message should contain 'ERROR'"); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * Stubs all charity-related columns on the mock ResultSet. + */ + private void stubCharityColumns(String uuid, String orgNumber, String name, + String link, boolean preApproved, String status) + throws SQLException { + when(mockResultSet.getString("UUID_charities")).thenReturn(uuid); + when(mockResultSet.getString("org_number")).thenReturn(orgNumber); + when(mockResultSet.getString("charity_name")).thenReturn(name); + when(mockResultSet.getString("charity_link")).thenReturn(link); + when(mockResultSet.getBoolean("pre_approved")).thenReturn(preApproved); + when(mockResultSet.getString("status")).thenReturn(status); + } + + /** + * Stubs all donation-related columns on the mock ResultSet. + */ + private void stubDonationColumns(String uuid, double amount, LocalDate date) + throws SQLException { + when(mockResultSet.getString("UUID_Donations")).thenReturn(uuid); + when(mockResultSet.getDouble("amount")).thenReturn(amount); + Date sqlDate = Date.valueOf(date); + when(mockResultSet.getDate("date")).thenReturn(sqlDate); + } +} \ No newline at end of file