diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/CharitySelect.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/CharitySelect.java index bdc0432..4c9ffb1 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/CharitySelect.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/database/Readers/CharitySelect.java @@ -1,30 +1,57 @@ package ntnu.systemutvikling.team6.database.Readers; +import java.sql.*; +import java.time.LocalDate; +import java.util.ArrayList; import ntnu.systemutvikling.team6.database.DatabaseConnection; import ntnu.systemutvikling.team6.models.Charity; import ntnu.systemutvikling.team6.models.CharityRegistry; import ntnu.systemutvikling.team6.models.Feedback; import ntnu.systemutvikling.team6.models.user.User; -import java.sql.*; -import java.time.LocalDate; -import java.util.ArrayList; - +/** + * Data access class responsible for reading charity-related data from the database. + * + *

Provides methods to retrieve all charities (with their associated feedback and users) as well + * as feedback entries for a specific charity by UUID. + * + *

All queries are executed against a MySQL database via a {@link DatabaseConnection}. + */ public class CharitySelect { - private final DatabaseConnection connection; + private final DatabaseConnection connection; - public CharitySelect(DatabaseConnection connection){ - this.connection = connection; - } + /** + * Constructs a new {@code CharitySelect} with the given database connection. + * + * @param connection the {@link DatabaseConnection} to use for executing queries; must not be + * {@code null} + */ + public CharitySelect(DatabaseConnection connection) { + this.connection = connection; + } - public CharityRegistry getCharitiesFromDB() { - CharityRegistry registry = null; - Connection conn = null; - try { - conn = connection.getMySqlConnection(); - String sql_query = - """ - SELECT + /** + * Retrieves all charities from the database, including their associated feedback and the users + * who submitted each piece of feedback. + * + *

The query performs a LEFT JOIN between the {@code Charities}, {@code Feedback}, and {@code + * User} tables. Each unique charity is added once to the registry; any feedback rows found for + * that charity are appended to its feedback list. + * + *

Note: charities with no feedback are still included in the result due to the LEFT JOIN. + * + * @return a {@link CharityRegistry} containing all charities found in the database, each + * populated with its associated {@link Feedback} objects (if any) + * @throws RuntimeException if a {@link SQLException} occurs while executing the query + */ + public CharityRegistry getCharitiesFromDB() { + CharityRegistry registry = null; + Connection conn = null; + try { + conn = connection.getMySqlConnection(); + String sql_query = + """ + SELECT c.UUID_charities, c.org_number, c.charity_name, c.charity_link, c.pre_approved, c.status, f.UUID_feedback, f.feedback_comment, f.feedback_date, f.isAnonymous, f.charity_id, f.user_id, u.UUID_user, u.user_name, u.user_email, u.user_password, u.role @@ -32,95 +59,110 @@ public CharityRegistry getCharitiesFromDB() { LEFT JOIN Feedback f ON f.charity_id = c.UUID_charities LEFT JOIN User u ON f.user_id = u.UUID_user """; - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery(sql_query); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql_query); - Charity currentCharity = null; - String lastCharity = null; + Charity currentCharity = null; + String lastCharity = null; - registry = new CharityRegistry(); - while (rs.next()) { - String currentId = rs.getString("UUID_charities"); + registry = new CharityRegistry(); + while (rs.next()) { + String currentId = rs.getString("UUID_charities"); - if (lastCharity == null || !currentId.equals(lastCharity)) { - currentCharity = - new Charity( - rs.getString("UUID_charities"), - rs.getString("org_number"), - rs.getString("charity_link"), - rs.getString("charity_name"), - rs.getBoolean("pre_approved"), - rs.getString("status") - ); - registry.addCharity(currentCharity); - lastCharity = currentId; - } - String feedbackId = rs.getString("UUID_feedback"); - if (feedbackId != null){ - User userWithNoSettingsAndInbox = new User( - rs.getString("UUID_User"), - rs.getString("user_name"), - rs.getString("user_email"), - rs.getString("user_password"), - rs.getString("role") - ); + if (lastCharity == null || !currentId.equals(lastCharity)) { + currentCharity = + new Charity( + rs.getString("UUID_charities"), + rs.getString("org_number"), + rs.getString("charity_link"), + rs.getString("charity_name"), + rs.getBoolean("pre_approved"), + rs.getString("status")); + registry.addCharity(currentCharity); + lastCharity = currentId; + } + String feedbackId = rs.getString("UUID_feedback"); + if (feedbackId != null) { + User userWithNoSettingsAndInbox = + new User( + rs.getString("UUID_User"), + rs.getString("user_name"), + rs.getString("user_email"), + rs.getString("user_password"), + rs.getString("role")); - Feedback feedback = new Feedback( - rs.getString("UUID_feedback"), - userWithNoSettingsAndInbox, - rs.getString("feedback_comment"), - LocalDate.parse(rs.getString("feedback_date")) - ); + Feedback feedback = + new Feedback( + rs.getString("UUID_feedback"), + userWithNoSettingsAndInbox, + rs.getString("feedback_comment"), + LocalDate.parse(rs.getString("feedback_date"))); - currentCharity.getFeedbacks().add(feedback); - } - } - } catch (SQLException e) { - e.printStackTrace(); - throw new RuntimeException("ERROR: Something went wrong during updating charities table."); + currentCharity.getFeedbacks().add(feedback); } - return registry; + } + } catch (SQLException e) { + e.printStackTrace(); + throw new RuntimeException("ERROR: Something went wrong during updating charities table."); } - public ArrayList getFeedbackforCharityUUID(String charity_uuid) { - ArrayList Feedbacks = new ArrayList<>(); - Connection conn = null; - try { - conn = connection.getMySqlConnection(); - String sql_query = - """ + return registry; + } + + /** + * A helper function that retrieves all feedback entries associated with a specific charity, + * identified by its UUID. Currently, has no use. + * + *

Each {@link Feedback} object is populated with the associated {@link User} (without settings + * or inbox data). The query uses a LEFT JOIN between the {@code Feedback} and {@code User} + * tables, filtered by {@code charity_id}. + * + * @param charity_uuid the UUID of the charity whose feedback should be retrieved; must not be + * {@code null} + * @return an {@link ArrayList} of {@link Feedback} objects for the given charity; returns an + * empty list if no feedback exists for that charity + * @throws RuntimeException if any exception occurs while executing the query, wrapping the + * original cause + */ + public ArrayList getFeedbackforCharityUUID(String charity_uuid) { + ArrayList Feedbacks = new ArrayList<>(); + Connection conn = null; + try { + conn = connection.getMySqlConnection(); + String sql_query = + """ SELECT f.UUID_feedback, f.feedback_comment, f.feedback_date, f.isAnonymous, f.charity_id, f.user_id, - u.UUID_user, u.user_name, u.user_email, u.user_password, u.role + u.UUID_user, u.user_name, u.user_email, u.user_password, u.role FROM Feedback f - LEFT JOIN User u ON f.user_id = u.UUID_user + LEFT JOIN User u ON f.user_id = u.UUID_user WHERE f.charity_id = ?; """; - PreparedStatement stmt = conn.prepareStatement(sql_query); - stmt.setString(1, charity_uuid); - ResultSet rs = stmt.executeQuery(); + PreparedStatement stmt = conn.prepareStatement(sql_query); + stmt.setString(1, charity_uuid); + ResultSet rs = stmt.executeQuery(); - while (rs.next()){ - User userWithNoSettingsAndInbox = new User( - rs.getString("UUID_User"), - rs.getString("user_name"), - rs.getString("user_email"), - rs.getString("user_password"), - rs.getString("role") - ); - Feedback feedback = new Feedback( - rs.getString("UUID_feedback"), - userWithNoSettingsAndInbox, - rs.getString("feedback_comment"), - LocalDate.parse(rs.getString("feedback_date")) - ); - Feedbacks.add(feedback); - } - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } finally { - conn = null; - } - return Feedbacks; + while (rs.next()) { + User userWithNoSettingsAndInbox = + new User( + rs.getString("UUID_User"), + rs.getString("user_name"), + rs.getString("user_email"), + rs.getString("user_password"), + rs.getString("role")); + Feedback feedback = + new Feedback( + rs.getString("UUID_feedback"), + userWithNoSettingsAndInbox, + rs.getString("feedback_comment"), + LocalDate.parse(rs.getString("feedback_date"))); + Feedbacks.add(feedback); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } finally { + conn = null; } + return Feedbacks; + } } diff --git a/helpmehelpapplication/src/test/java/ntnu/systemutvikling/team6/database/Readers/CharitySelectTest.java b/helpmehelpapplication/src/test/java/ntnu/systemutvikling/team6/database/Readers/CharitySelectTest.java new file mode 100644 index 0000000..7e4bad0 --- /dev/null +++ b/helpmehelpapplication/src/test/java/ntnu/systemutvikling/team6/database/Readers/CharitySelectTest.java @@ -0,0 +1,290 @@ +package ntnu.systemutvikling.team6.database.Readers; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.sql.*; +import java.util.ArrayList; +import ntnu.systemutvikling.team6.database.DatabaseConnection; +import ntnu.systemutvikling.team6.models.Charity; +import ntnu.systemutvikling.team6.models.CharityRegistry; +import ntnu.systemutvikling.team6.models.Feedback; +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; + +/** + * Unit tests for {@link CharitySelect}. + * + *

Uses Mockito to mock {@link DatabaseConnection}, {@link Connection}, {@link Statement}, {@link + * PreparedStatement}, and {@link ResultSet} so that no real database connection is required. + */ +@ExtendWith(MockitoExtension.class) +class CharitySelectTest { + + @Mock private DatabaseConnection mockDatabaseConnection; + @Mock private Connection mockConnection; + @Mock private Statement mockStatement; + @Mock private PreparedStatement mockPreparedStatement; + @Mock private ResultSet mockResultSet; + + private CharitySelect charitySelect; + + @BeforeEach + void setUp() { + charitySelect = new CharitySelect(mockDatabaseConnection); + } + + // ------------------------------------------------------------------------- + // getCharitiesFromDB + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getCharitiesFromDB – empty result set returns an empty registry") + void getCharitiesFromDB_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); + + CharityRegistry registry = charitySelect.getCharitiesFromDB(); + + assertNotNull(registry); + assertTrue( + registry.getAllCharities().isEmpty(), + "Registry should contain no charities when the result set is empty"); + } + + @Test + @DisplayName("getCharitiesFromDB – single charity with no feedback is added once") + void getCharitiesFromDB_singleCharityNoFeedback_addedOnce() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + // One row, no feedback + when(mockResultSet.next()).thenReturn(true, false); + when(mockResultSet.getString("UUID_charities")).thenReturn("charity-uuid-1"); + when(mockResultSet.getString("org_number")).thenReturn("123456789"); + when(mockResultSet.getString("charity_link")).thenReturn("https://example.org"); + when(mockResultSet.getString("charity_name")).thenReturn("Test Charity"); + when(mockResultSet.getBoolean("pre_approved")).thenReturn(true); + when(mockResultSet.getString("status")).thenReturn("ACTIVE"); + when(mockResultSet.getString("UUID_feedback")).thenReturn(null); + + CharityRegistry registry = charitySelect.getCharitiesFromDB(); + + assertEquals( + 1, registry.getAllCharities().size(), "Registry should contain exactly one charity"); + Charity charity = registry.getAllCharities().get(0); + assertEquals("Test Charity", charity.getName()); + assertTrue(charity.getFeedbacks().isEmpty(), "Charity should have no feedback"); + } + + @Test + @DisplayName("getCharitiesFromDB – single charity with one feedback entry is populated correctly") + void getCharitiesFromDB_singleCharityWithFeedback_feedbackAdded() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + // One row with feedback + when(mockResultSet.next()).thenReturn(true, false); + when(mockResultSet.getString("UUID_charities")).thenReturn("charity-uuid-1"); + when(mockResultSet.getString("org_number")).thenReturn("123456789"); + when(mockResultSet.getString("charity_link")).thenReturn("https://example.org"); + when(mockResultSet.getString("charity_name")).thenReturn("Test Charity"); + when(mockResultSet.getBoolean("pre_approved")).thenReturn(false); + when(mockResultSet.getString("status")).thenReturn("PENDING"); + + when(mockResultSet.getString("UUID_feedback")).thenReturn("feedback-uuid-1"); + when(mockResultSet.getString("UUID_User")).thenReturn("user-uuid-1"); + when(mockResultSet.getString("user_name")).thenReturn("Alice"); + when(mockResultSet.getString("user_email")).thenReturn("alice@example.com"); + when(mockResultSet.getString("user_password")).thenReturn("hashedpw"); + when(mockResultSet.getString("role")).thenReturn("USER"); + when(mockResultSet.getString("feedback_comment")).thenReturn("Great work!"); + when(mockResultSet.getString("feedback_date")).thenReturn("2024-03-15"); + + CharityRegistry registry = charitySelect.getCharitiesFromDB(); + + assertEquals(1, registry.getAllCharities().size()); + Charity charity = registry.getAllCharities().get(0); + assertEquals( + 1, charity.getFeedbacks().size(), "Charity should have exactly one feedback entry"); + + Feedback feedback = charity.getFeedbacks().get(0); + assertEquals("feedback-uuid-1", feedback.getFeedbackId()); + assertEquals("Great work!", feedback.getComment()); + } + + @Test + @DisplayName("getCharitiesFromDB – two different charities across two rows are both added") + void getCharitiesFromDB_twoCharities_bothAdded() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + + // First row: charity A, no feedback + // Second row: charity B, no feedback + when(mockResultSet.next()).thenReturn(true, true, false); + when(mockResultSet.getString("UUID_charities")).thenReturn("charity-uuid-A", "charity-uuid-B"); + when(mockResultSet.getString("org_number")).thenReturn("111111111", "222222222"); + when(mockResultSet.getString("charity_link")).thenReturn("https://a.org", "https://b.org"); + when(mockResultSet.getString("charity_name")).thenReturn("Charity A", "Charity B"); + when(mockResultSet.getBoolean("pre_approved")).thenReturn(true, false); + when(mockResultSet.getString("status")).thenReturn("ACTIVE", "INACTIVE"); + when(mockResultSet.getString("UUID_feedback")).thenReturn(null, null); + + CharityRegistry registry = charitySelect.getCharitiesFromDB(); + + assertEquals(2, registry.getAllCharities().size(), "Registry should contain two charities"); + } + + @Test + @DisplayName( + "getCharitiesFromDB – same charity UUID across two rows adds feedback without duplicating the charity") + void getCharitiesFromDB_sameCharityTwoRows_onlyOneCharityWithTwoFeedbacks() 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); + // Both rows share the same charity UUID + when(mockResultSet.getString("UUID_charities")).thenReturn("charity-uuid-1"); + when(mockResultSet.getString("org_number")).thenReturn("123456789"); + when(mockResultSet.getString("charity_link")).thenReturn("https://example.org"); + when(mockResultSet.getString("charity_name")).thenReturn("Test Charity"); + when(mockResultSet.getBoolean("pre_approved")).thenReturn(true); + when(mockResultSet.getString("status")).thenReturn("ACTIVE"); + + when(mockResultSet.getString("UUID_feedback")).thenReturn("feedback-uuid-1", "feedback-uuid-2"); + when(mockResultSet.getString("UUID_User")).thenReturn("user-uuid-1"); + when(mockResultSet.getString("user_name")).thenReturn("Alice"); + when(mockResultSet.getString("user_email")).thenReturn("alice@example.com"); + when(mockResultSet.getString("user_password")).thenReturn("hashedpw"); + when(mockResultSet.getString("role")).thenReturn("USER"); + when(mockResultSet.getString("feedback_comment")).thenReturn("First comment", "Second comment"); + when(mockResultSet.getString("feedback_date")).thenReturn("2024-03-15"); + + CharityRegistry registry = charitySelect.getCharitiesFromDB(); + + assertEquals(1, registry.getAllCharities().size(), "The same charity should not be duplicated"); + assertEquals( + 2, + registry.getAllCharities().get(0).getFeedbacks().size(), + "Both feedback entries should be attached to the single charity"); + } + + @Test + @DisplayName("getCharitiesFromDB – SQLException is wrapped in RuntimeException") + void getCharitiesFromDB_sqlException_throwsRuntimeException() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.createStatement()).thenThrow(new SQLException("DB error")); + + assertThrows( + RuntimeException.class, + () -> charitySelect.getCharitiesFromDB(), + "A SQLException should be rethrown as a RuntimeException"); + } + + // ------------------------------------------------------------------------- + // getFeedbackforCharityUUID + // ------------------------------------------------------------------------- + + @Test + @DisplayName("getFeedbackforCharityUUID – empty result set returns empty list") + void getFeedbackforCharityUUID_emptyResultSet_returnsEmptyList() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + when(mockResultSet.next()).thenReturn(false); + + ArrayList result = charitySelect.getFeedbackforCharityUUID("charity-uuid-1"); + + assertNotNull(result); + assertTrue( + result.isEmpty(), "Should return an empty list when no feedback exists for the given UUID"); + } + + @Test + @DisplayName("getFeedbackforCharityUUID – one row returns one Feedback with correct data") + void getFeedbackforCharityUUID_oneRow_returnsSingleFeedback() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, false); + when(mockResultSet.getString("UUID_feedback")).thenReturn("feedback-uuid-1"); + when(mockResultSet.getString("UUID_User")).thenReturn("user-uuid-1"); + when(mockResultSet.getString("user_name")).thenReturn("Bob"); + when(mockResultSet.getString("user_email")).thenReturn("bob@example.com"); + when(mockResultSet.getString("user_password")).thenReturn("secret"); + when(mockResultSet.getString("role")).thenReturn("ADMIN"); + when(mockResultSet.getString("feedback_comment")).thenReturn("Very helpful!"); + when(mockResultSet.getString("feedback_date")).thenReturn("2024-06-01"); + + ArrayList result = charitySelect.getFeedbackforCharityUUID("charity-uuid-1"); + + assertEquals(1, result.size()); + Feedback feedback = result.get(0); + assertEquals("feedback-uuid-1", feedback.getFeedbackId()); + assertEquals("Very helpful!", feedback.getComment()); + assertEquals("Bob", feedback.getUser().getName()); + } + + @Test + @DisplayName("getFeedbackforCharityUUID – two rows returns two Feedback objects") + void getFeedbackforCharityUUID_twoRows_returnsTwoFeedbacks() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + + when(mockResultSet.next()).thenReturn(true, true, false); + when(mockResultSet.getString("UUID_feedback")).thenReturn("fb-uuid-1", "fb-uuid-2"); + when(mockResultSet.getString("UUID_User")).thenReturn("user-uuid-1"); + when(mockResultSet.getString("user_name")).thenReturn("Carol"); + when(mockResultSet.getString("user_email")).thenReturn("carol@example.com"); + when(mockResultSet.getString("user_password")).thenReturn("pw"); + when(mockResultSet.getString("role")).thenReturn("USER"); + when(mockResultSet.getString("feedback_comment")).thenReturn("Comment one", "Comment two"); + when(mockResultSet.getString("feedback_date")).thenReturn("2024-07-10"); + + ArrayList result = charitySelect.getFeedbackforCharityUUID("charity-uuid-1"); + + assertEquals( + 2, result.size(), "Should return exactly two Feedback objects for two result rows"); + } + + @Test + @DisplayName("getFeedbackforCharityUUID – UUID is bound to the PreparedStatement parameter") + void getFeedbackforCharityUUID_correctUUIDBindingVerified() throws Exception { + String targetUuid = "charity-uuid-XYZ"; + + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + when(mockResultSet.next()).thenReturn(false); + + charitySelect.getFeedbackforCharityUUID(targetUuid); + + verify(mockPreparedStatement).setString(1, targetUuid); + } + + @Test + @DisplayName("getFeedbackforCharityUUID – exception during query is wrapped in RuntimeException") + void getFeedbackforCharityUUID_exception_throwsRuntimeException() throws Exception { + when(mockDatabaseConnection.getMySqlConnection()).thenReturn(mockConnection); + when(mockConnection.prepareStatement(anyString())) + .thenThrow(new SQLException("Prepared statement failed")); + + assertThrows( + RuntimeException.class, + () -> charitySelect.getFeedbackforCharityUUID("charity-uuid-1"), + "Any exception during query execution should be rethrown as RuntimeException"); + } +}