From ea5cf76b6b06b0eb66aba9c4ae992f5c831be7c1 Mon Sep 17 00:00:00 2001 From: lukaceli Date: Tue, 7 Apr 2026 15:11:07 +0200 Subject: [PATCH 1/2] Added javadoc to DonationDao and UserStatistics --- .../java/application/user/UserStatistics.java | 35 ++++++++ src/main/java/persistence/DonationDao.java | 87 ++++++++++++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/main/java/application/user/UserStatistics.java b/src/main/java/application/user/UserStatistics.java index 64333f1..5fe67d4 100644 --- a/src/main/java/application/user/UserStatistics.java +++ b/src/main/java/application/user/UserStatistics.java @@ -5,28 +5,63 @@ import java.util.List; import persistence.DonationDao; +/** + * Provides statistical information about a user's donation activity. + * Acts as a service layer between the application and {@link DonationDao}, + * exposing user-friendly methods for retrieving donation history, + * total amounts, and favorite organizations. + */ public class UserStatistics { private DonationDao donationDao; + /** + * Constructs a new {@code UserStatistics} instance with a default {@link DonationDao}. + */ public UserStatistics() { this.donationDao = new DonationDao(); } + /** + * Returns the top three organizations that the user has donated to most frequently. + * + * @param user the user whose favorite organizations are to be retrieved; must not be null + * @return a list of up to three formatted strings, each representing an organization + * and its donation count; empty list if the user has no donations + */ public List userFavoriteOrganizations(User user) { List favoriteOrganizations = donationDao.getFavoriteOrganizations(user.getID()); return favoriteOrganizations; } + /** + * Returns a formatted list of all donations made by the user. + * Each entry contains the donation amount, date, and organization. + * + * @param user the user whose donation history is to be retrieved; must not be null + * @return a list of formatted donation strings; empty list if the user has no donations + */ public List userDonations(User user) { List userDonationList = donationDao.getUserDonations(user.getID()); return userDonationList; } + /** + * Returns the total amount donated by the user across all donations. + * + * @param user the user whose total donation amount is to be calculated; must not be null + * @return a string representation of the total donated amount; {@code "0"} if no donations exist + */ public String userTotalDonationAmount(User user) { String totalDonations = donationDao.getTotalDonationAmount(user.getID()); return totalDonations; } + /** + * Returns the total number of donations made by the user. + * + * @param user the user whose donation count is to be retrieved; must not be null + * @return a string representation of the total number of donations; {@code "0"} if none exist + */ public String getTotalDonationsMade(User user) { String totalDonationsMade = donationDao.getTotalDonationsMade(user.getID()); return totalDonationsMade; diff --git a/src/main/java/persistence/DonationDao.java b/src/main/java/persistence/DonationDao.java index 70ddba6..0361f2b 100644 --- a/src/main/java/persistence/DonationDao.java +++ b/src/main/java/persistence/DonationDao.java @@ -13,8 +13,20 @@ import java.util.List; import persistence.db.Database; +/** + * Data Access Object for donation-related database operations. + * Provides methods for inserting donations and retrieving donation statistics + * such as history, total amounts, and favorite organizations for a given user. + */ public class DonationDao { - + /** + * Inserts a new donation record into the database. + * Upon successful insertion, the generated database ID is set on the donation object. + * + * @param donation the {@link Donation} to insert; must not be null and must have + * a valid amount, user, and organization + * @throws RuntimeException if a database error occurs during the insert + */ public void insert(Donation donation) { String sql = "INSERT INTO donation(amount, donation_date" + ", user_id, organization_id) VALUES(?, ?, ?, ?)"; @@ -38,6 +50,15 @@ public void insert(Donation donation) { } } + /** + * Maps a single row from the provided {@code ResultSet} into a {@link Donation} object. + * The method extracts information about the donation, user, and organization + * from the {@code ResultSet} and constructs corresponding objects. + * + * @param rs the {@code ResultSet} containing the data for a single row; must not be null + * @return a {@code Donation} object populated with data from the row in the {@code ResultSet} + * @throws SQLException if a database access error occurs or if the column labels cannot be found + */ private Donation mapRow(ResultSet rs) throws SQLException { User user = new User( rs.getString("user_name"), @@ -64,6 +85,15 @@ private Donation mapRow(ResultSet rs) throws SQLException { return donation; } + /** + * Retrieves a list of donations made by a specific user, based on the user ID. + * The donations include details such as the donation amount, date, associated user, + * and organization. + * + * @param userId the unique identifier of the user whose donations are to be retrieved + * @return a list of {@code Donation} objects representing donations made by the user + * @throws RuntimeException if an error occurs during the database operation + */ public List findByUser(long userId) { String sql = """ SELECT d.id, d.amount, d.donation_date, @@ -88,6 +118,14 @@ public List findByUser(long userId) { } } + /** + * Retrieves a formatted list of donations made by a specific user. + * Each entry is a string on the form {@code "Amount: X, Date: Y, Organization: Z"}. + * + * @param userId the unique identifier of the user whose donations are to be retrieved + * @return a list of formatted strings, one per donation; empty list if none found + * @throws RuntimeException if a database error occurs during the query + */ public List getUserDonations(long userId) { String sql = """ SELECT amount, donation_date, organization_id @@ -108,6 +146,16 @@ public List getUserDonations(long userId) { } } + /** + * Retrieves a list of the user's favorite organizations based on donation history. + * The method queries the database to find the organizations the user has donated to most frequently + * and limits the result to the top three organizations. + * + * @param userId the unique identifier of the user whose favorite organizations are to be retrieved + * @return a list of strings representing the top three organizations donated to by the user, + * ordered by the number of donations in descending order + * @throws RuntimeException if an error occurs while querying the database + */ public List getFavoriteOrganizations(long userId) { String sql = """ SELECT organization_id, COUNT(*) AS donation_count @@ -131,6 +179,15 @@ SELECT organization_id, COUNT(*) AS donation_count } } + /** + * Retrieves the total donation amount made by a specific user. + * The method calculates the sum of all donations associated with the given user ID. + * + * @param userId the unique identifier of the user whose total donation amount is to be retrieved + * @return a string representing the total donation amount made by the user; if no donations exist, + * the method returns "0" + * @throws RuntimeException if an error occurs during database access + */ public String getTotalDonationAmount(long userId) { String sql = """ SELECT SUM(CAST(amount as REAL)) @@ -153,6 +210,17 @@ SELECT SUM(CAST(amount as REAL)) } } + /** + * Retrieves the total number of donations made by a specific user. + * The method counts the number of donation records associated with the provided user ID + * in the database and returns the total count as a string. + * + * @param userId the unique identifier of the user whose total number of donations + * is to be retrieved + * @return a string representing the total count of donations made by the user; + * returns "0" if no donations exist + * @throws RuntimeException if a database access error occurs + */ public String getTotalDonationsMade(long userId) { String sql = """ SELECT COUNT(*) @@ -177,12 +245,29 @@ SELECT COUNT(*) } + /** + * Maps a single row from the provided {@code ResultSet} into a string representation + * consisting of the donation amount, date, and organization ID. + * + * @param rs the {@code ResultSet} containing the data for a single row; must not be null + * @return a string representing the donation with details such as amount, date, + * and organization ID + * @throws SQLException if a database access error occurs or if the column labels cannot be found + */ private String mapRowSimple(ResultSet rs) throws SQLException { return "Amount: " + rs.getString("amount") + ", Date: " + rs.getString("donation_date") + ", Organization: " + rs.getString("organization_id"); } + /** + * Maps a single row from the provided {@code ResultSet} into a formatted string + * combining the organization ID and its donation count. + * + * @param rs the {@code ResultSet} containing the data for a single row; must not be null + * @return a string on the form {@code "organization_id (N donations)"} + * @throws SQLException if a database access error occurs or a column label cannot be found + */ private String mapRowFavorite(ResultSet rs) throws SQLException { return rs.getString("organization_id") + " (" + rs.getInt("donation_count") + " donasjoner)"; From 621da6c8e1bdb1bd938ffeda4f827854f83b6b16 Mon Sep 17 00:00:00 2001 From: haavajor Date: Wed, 15 Apr 2026 09:45:45 +0200 Subject: [PATCH 2/2] Unit tests --- src/main/java/application/user/UserLogin.java | 2 +- .../java/application/user/UserLoginTest.java | 162 +++++++++++++++++ .../application/user/UserRegisterTest.java | 170 ++++++++++++++++++ .../application/user/UserStatisticsTest.java | 132 ++++++++++++++ .../security/Sha256PasswordHasherTest.java | 26 +++ 5 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 src/test/java/application/user/UserLoginTest.java create mode 100644 src/test/java/application/user/UserRegisterTest.java create mode 100644 src/test/java/application/user/UserStatisticsTest.java create mode 100644 src/test/java/integration/security/Sha256PasswordHasherTest.java diff --git a/src/main/java/application/user/UserLogin.java b/src/main/java/application/user/UserLogin.java index e116498..50940ac 100644 --- a/src/main/java/application/user/UserLogin.java +++ b/src/main/java/application/user/UserLogin.java @@ -16,7 +16,7 @@ public UserLogin(PasswordHasher hasher, UserRepository repo) { public User execute(String login, String password) { // normalize (trim etc.) - login.trim().toLowerCase(); + login = login.trim().toLowerCase(); User user = repo.findByLogin(login) .orElseThrow(() -> new IllegalArgumentException("Invalid credentials")); diff --git a/src/test/java/application/user/UserLoginTest.java b/src/test/java/application/user/UserLoginTest.java new file mode 100644 index 0000000..4bd8312 --- /dev/null +++ b/src/test/java/application/user/UserLoginTest.java @@ -0,0 +1,162 @@ +package application.user; + +import application.security.PasswordHasher; +import domain.user.User; +import org.junit.jupiter.api.Test; +import persistence.UserRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class UserLoginTest { + + @Test + void executeReturnsUserWhenCredentialsAreValid() { + User user = new User("testuser", "12345678", "hashed-secret", "test@example.com"); + PasswordHasher hasher = new FakePasswordHasher("hashed-secret"); + UserRepository repo = new FakeUserRepository(Optional.of(user)); + + UserLogin userLogin = new UserLogin(hasher, repo); + + User result = userLogin.execute("testuser", "secret"); + + assertSame(user, result); + + } + + @Test + void executeThrowsWhenLoginDoesNotExist() { + PasswordHasher hasher = new FakePasswordHasher("hashed-secret"); + UserRepository repo = new FakeUserRepository(Optional.empty()); + + UserLogin userLogin = new UserLogin(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userLogin.execute("missing-user", "secret") + ); + + assertEquals("Invalid credentials", exception.getMessage()); + } + + @Test + void executeThrowsWhenPasswordIsWrong() { + User user = new User("testuser", "12345678", "stored-hash", "test@example.com"); + PasswordHasher hasher = new FakePasswordHasher("different-hash"); + UserRepository repo = new FakeUserRepository(Optional.of(user)); + + UserLogin userLogin = new UserLogin(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userLogin.execute("testuser", "wrong-password") + ); + + assertEquals("Invalid password", exception.getMessage()); + } + + @Test + void executeUsesNormalizedLoginBeforeLookup() { + User user = new User("testuser", "12345678", "hashed-secret", "test@example.com"); + RecordingUserRepository repo = new RecordingUserRepository(Optional.of(user)); + PasswordHasher hasher = new FakePasswordHasher("hashed-secret"); + + UserLogin userLogin = new UserLogin(hasher, repo); + + userLogin.execute(" TestUser ", "secret"); + + assertEquals("testuser", repo.lastLoginUsed); + } + + private static class FakePasswordHasher implements PasswordHasher{ + private final String hashToReturn; + private FakePasswordHasher(String hashToReturn) { + this.hashToReturn = hashToReturn; + } + + @Override + public String hash(String password) { + return hashToReturn; + } + } + + private static class FakeUserRepository implements UserRepository { + private final Optional userToReturn; + + private FakeUserRepository(Optional userToReturn) { + this.userToReturn = userToReturn; + } + + @Override + public Optional findByUsername(String username){ + return Optional.empty(); + } + + @Override + public Optional findByEmail(String email){ + return Optional.empty(); + } + + @Override + public Optional findByLogin(String login) { + return userToReturn; + } + + @Override + public boolean existsByEmail(String email) { + return false; + } + + @Override + public boolean existsByUsername(String username) { + return false; + } + + @Override + public void insert(User user) { + } + } + + private static class RecordingUserRepository implements UserRepository { + private final Optional userToReturn; + private String lastLoginUsed; + + private RecordingUserRepository(Optional userToReturn) { + this.userToReturn = userToReturn; + } + + @Override + public Optional findByUsername(String username) { + return Optional.empty(); + } + + @Override + public Optional findByEmail(String email) { + return Optional.empty(); + } + + @Override + public Optional findByLogin(String login) { + lastLoginUsed = login; + return userToReturn; + } + + @Override + public boolean existsByEmail(String email) { + return false; + } + + @Override + public boolean existsByUsername(String username) { + return false; + } + + @Override + public void insert(User user) { + } + } + + +} + diff --git a/src/test/java/application/user/UserRegisterTest.java b/src/test/java/application/user/UserRegisterTest.java new file mode 100644 index 0000000..c5296cd --- /dev/null +++ b/src/test/java/application/user/UserRegisterTest.java @@ -0,0 +1,170 @@ +package application.user; + +import application.security.PasswordHasher; +import domain.user.User; +import org.junit.jupiter.api.Test; +import persistence.UserRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class UserRegisterTest { + + @Test + void executeInsertsUserWhenInputIsValid() { + RecordingUserRepository repo = new RecordingUserRepository(); + PasswordHasher hasher = new FakePasswordHasher("hashed-password"); + UserRegister userRegister = new UserRegister(hasher, repo); + + userRegister.execute("testuser", "12345678", "secret", "test@example.com"); + + assertNotNull(repo.insertedUser); + assertEquals("testuser", repo.insertedUser.getUsername()); + assertEquals("12345678", repo.insertedUser.getPhoneNumber()); + assertEquals("hashed-password", repo.insertedUser.getPassword()); + assertEquals("test@example.com", repo.insertedUser.getEMail()); + } + + + @Test + void executeNormalizesUsernameEmailAndPhoneBeforeInsert() { + RecordingUserRepository repo = new RecordingUserRepository(); + PasswordHasher hasher = new FakePasswordHasher("hashed-password"); + UserRegister userRegister = new UserRegister(hasher, repo); + + userRegister.execute(" testuser ", " 12345678 ", "secret", " TEST@EXAMPLE.COM "); + + assertNotNull(repo.insertedUser); + assertEquals("testuser", repo.insertedUser.getUsername()); + assertEquals("12345678", repo.insertedUser.getPhoneNumber()); + assertEquals("test@example.com", repo.insertedUser.getEMail()); + } + + + @Test + void executeThrowsWhenEmailAlreadyExists() { + RecordingUserRepository repo = new RecordingUserRepository(); + repo.emailExists = true; + PasswordHasher hasher = new FakePasswordHasher("hashed-password"); + UserRegister userRegister = new UserRegister(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userRegister.execute("testuser", "12345678", "secret", "test@example.com") + ); + + assertEquals("Email already in use", exception.getMessage()); + assertNull(repo.insertedUser); + } + + @Test + void executeThrowsWhenUsernameAlreadyExists() { + RecordingUserRepository repo = new RecordingUserRepository(); + repo.usernameExists = true; + PasswordHasher hasher = new FakePasswordHasher("hashed-password"); + UserRegister userRegister = new UserRegister(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userRegister.execute("testuser", "12345678", "secret", "test@example.com") + ); + + assertEquals("This username is taken", exception.getMessage()); + assertNull(repo.insertedUser); + } + + @Test + void executeHashesPasswordBeforeSavingUser() { + RecordingUserRepository repo = new RecordingUserRepository(); + RecordingPasswordHasher hasher = new RecordingPasswordHasher("hashed-password"); + UserRegister userRegister = new UserRegister(hasher, repo); + + userRegister.execute("testuser", "12345678", "secret", "test@example.com"); + + assertEquals("secret", hasher.lastPasswordInput); + assertNotNull(repo.insertedUser); + assertEquals("hashed-password", repo.insertedUser.getPassword()); + } + + @Test + void executePropagatesDomainValidationErrors() { + RecordingUserRepository repo = new RecordingUserRepository(); + PasswordHasher hasher = new FakePasswordHasher("hashed-password"); + UserRegister userRegister = new UserRegister(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userRegister.execute("testuser", "1234", "secret", "test@example.com") + ); + + assertEquals("Fill in a phonenumber with 8 digits", exception.getMessage()); + assertNull(repo.insertedUser); + } + + private static class FakePasswordHasher implements PasswordHasher { + private final String hashToReturn; + + private FakePasswordHasher(String hashToReturn) { + this.hashToReturn = hashToReturn; + } + + @Override + public String hash(String password) { + return hashToReturn; + } + } + + + private static class RecordingPasswordHasher implements PasswordHasher { + private final String hashToReturn; + private String lastPasswordInput; + + private RecordingPasswordHasher(String hashToReturn) { + this.hashToReturn = hashToReturn; + } + + @Override + public String hash(String password) { + lastPasswordInput = password; + return hashToReturn; + } + } + + private static class RecordingUserRepository implements UserRepository { + private boolean emailExists; + private boolean usernameExists; + private User insertedUser; + + @Override + public Optional findByUsername(String username) { + return Optional.empty(); + } + + @Override + public Optional findByEmail(String email) { + return Optional.empty(); + } + + @Override + public Optional findByLogin(String login) { + return Optional.empty(); + } + + + @Override + public boolean existsByEmail(String email) { + return emailExists; + } + + @Override + public boolean existsByUsername(String username) { + return usernameExists; + } + + @Override + public void insert(User user) { + insertedUser = user; + } + } +} diff --git a/src/test/java/application/user/UserStatisticsTest.java b/src/test/java/application/user/UserStatisticsTest.java new file mode 100644 index 0000000..da904cb --- /dev/null +++ b/src/test/java/application/user/UserStatisticsTest.java @@ -0,0 +1,132 @@ +package application.user; + +import domain.user.User; +import org.junit.jupiter.api.Test; +import persistence.DonationDao; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserStatisticsTest { + + @Test + void userFavoriteOrganizationsReturnsListFromDonationDao() throws Exception { + FakeDonationDao fakeDao = new FakeDonationDao(); + fakeDao.favoriteOrganizationsToReturn = List.of( + "Org1 (3 donasjoner)", + "Org2 (2 donasjoner)" + ); + + UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); + User user = createUserWithId(42L); + + List result = statistics.userFavoriteOrganizations(user); + + assertEquals(List.of("Org1 (3 donasjoner)", "Org2 (2 donasjoner)"), result); + assertEquals(42L, fakeDao.lastUserIdForFavorites); + } + + @Test + void userDonationsReturnsListFromDonationDao() throws Exception { + FakeDonationDao fakeDao = new FakeDonationDao(); + fakeDao.userDonationsToReturn = List.of( + "Amount: 100, Date: 2026-04-15, Organization: Org1", + "Amount: 50, Date: 2026-04-16, Organization: Org2" + ); + + UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); + User user = createUserWithId(7L); + + List result = statistics.userDonations(user); + + assertEquals(List.of( + "Amount: 100, Date: 2026-04-15, Organization: Org1", + "Amount: 50, Date: 2026-04-16, Organization: Org2" + ), result); + assertEquals(7L, fakeDao.lastUserIdForDonations); + } + + @Test + void userTotalDonationAmountReturnsValueFromDonationDao() throws Exception { + FakeDonationDao fakeDao = new FakeDonationDao(); + fakeDao.totalDonationAmountToReturn = "250.00"; + + UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); + User user = createUserWithId(15L); + + String result = statistics.userTotalDonationAmount(user); + + assertEquals("250.00", result); + assertEquals(15L, fakeDao.lastUserIdForTotalAmount); + } + + @Test + void getTotalDonationsMadeReturnsValueFromDonationDao() throws Exception { + FakeDonationDao fakeDao = new FakeDonationDao(); + fakeDao.totalDonationsMadeToReturn = "5"; + + UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); + User user = createUserWithId(99L); + + String result = statistics.getTotalDonationsMade(user); + + assertEquals("5", result); + assertEquals(99L, fakeDao.lastUserIdForTotalCount); + } + + + + private UserStatistics createStatisticsWithFakeDao(FakeDonationDao fakeDao) throws Exception { + UserStatistics statistics = new UserStatistics(); + + Field field = UserStatistics.class.getDeclaredField("donationDao"); + field.setAccessible(true); + field.set(statistics, fakeDao); + + return statistics; + } + + private User createUserWithId(long id) { + User user = new User("testuser", "12345678", "password", "test@example.com"); + user.setId(id); + return user; + } + + private static class FakeDonationDao extends DonationDao { + private List favoriteOrganizationsToReturn = List.of(); + private List userDonationsToReturn = List.of(); + private String totalDonationAmountToReturn = "0"; + private String totalDonationsMadeToReturn = "0"; + + private long lastUserIdForFavorites; + private long lastUserIdForDonations; + private long lastUserIdForTotalAmount; + private long lastUserIdForTotalCount; + + @Override + public List getFavoriteOrganizations(long userId) { + lastUserIdForFavorites = userId; + return favoriteOrganizationsToReturn; + } + + @Override + public List getUserDonations(long userId) { + lastUserIdForDonations = userId; + return userDonationsToReturn; + } + + @Override + public String getTotalDonationAmount(long userId) { + lastUserIdForTotalAmount = userId; + return totalDonationAmountToReturn; + } + + @Override + public String getTotalDonationsMade(long userId) { + lastUserIdForTotalCount = userId; + return totalDonationsMadeToReturn; + } + } +} diff --git a/src/test/java/integration/security/Sha256PasswordHasherTest.java b/src/test/java/integration/security/Sha256PasswordHasherTest.java new file mode 100644 index 0000000..67b360e --- /dev/null +++ b/src/test/java/integration/security/Sha256PasswordHasherTest.java @@ -0,0 +1,26 @@ +package integration.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class Sha256PasswordHasherTest { + @Test + void hashReturnsExpectedSha256ForKnownInput() { + Sha256PasswordHasher hasher = new Sha256PasswordHasher(); + + String result = hasher.hash("password"); + + assertEquals("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", result); + } + + @Test + void hashReturns64CharacterLowercaseHexString() { + Sha256PasswordHasher hasher = new Sha256PasswordHasher(); + + String result = hasher.hash("secret"); + + assertEquals(64, result.length()); + assertTrue(result.matches("[0-9a-f]{64}")); + } +}