diff --git a/Documents/~$ftware Vision document - spring 2026.docx b/Documents/~$ftware Vision document - spring 2026.docx new file mode 100644 index 0000000..38d2bf9 Binary files /dev/null and b/Documents/~$ftware Vision document - spring 2026.docx differ diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index b6dd8e0..611e87f 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -20,20 +20,27 @@ import javafx.scene.Scene; import javafx.stage.Stage; +/** + * Entry point for the GiveHope application. + * Initializes the database, fetches organizations from Innsamlingskontrollen, + * and launches the JavaFX application. + */ public class Main extends Application{ public static void main(String[] args) { launch(args); } + /** + * Starts the JavaFX application by initializing the database, + * loading organizations from Innsamlingskontrollen, and displaying the main view. + * + * @param stage the primary stage for the application + * @throws Exception if the FXML file cannot be loaded + */ @Override public void start(Stage stage) throws Exception { Database.initializeDatabase(); - System.out.println("DB path: " + new java.io.File("givehope.db").getAbsolutePath()); - OrganizationDetails details = InnsamlingskontrollenClient.fetchDetails( - "https://www.innsamlingskontrollen.no/organisasjoner/caritas-norge/" - ); - System.out.println("Description: " + details.getDescription()); - System.out.println("Logo: " + details.getLogoUrl()); + OrganizationDao orgDao = new OrganizationDao(); Organization[] orgs = InnsamlingskontrollenClient.fetchOrganizations(); for (Organization org : orgs) { @@ -50,6 +57,12 @@ public void start(Stage stage) throws Exception { stage.show(); } + /** + * Inserts test users and donations into the database if they do not already exist. + * Used for development and demonstration purposes. + * + * @throws SQLException if a database error occurs + */ private void addTestdata() throws SQLException { UserDao userDao = new UserDao(); DonationDao donationDao = new DonationDao(); diff --git a/src/main/java/application/user/UserEditProfile.java b/src/main/java/application/user/UserEditProfile.java new file mode 100644 index 0000000..a6008fc --- /dev/null +++ b/src/main/java/application/user/UserEditProfile.java @@ -0,0 +1,47 @@ +package application.user; + +import application.security.PasswordHasher; +import domain.user.User; +import persistence.dao.UserDao; +import persistence.dao.UserRepository; + +/** + * Use case class for editing a user's profile information. + * Handles validation, optional password hashing and persistence of updated user data. + */ +public class UserEditProfile { + + private final PasswordHasher hasher; + private final UserRepository repo; + + /** + * Constructs a new {@code UserEditProfile} with the given hasher and repository. + * + * @param hasher the password hasher used to hash new passwords + * @param repo the repository used to persist updated user data + */ + public UserEditProfile(PasswordHasher hasher, UserRepository repo) { + this.hasher = hasher; + this.repo = repo; + } + + /** + * Updates the user's profile with the provided information. + * If the password field is non-blank, it will be hashed and updated. + * + * @param user the user to update + * @param username the new username + * @param email the new email address + * @param phoneNumber the new phone number + * @param password the new password, or blank/null to keep the current password + */ + public void execute(User user, String username, String email, String phoneNumber, String password) { + user.setUsername(username); + user.setEmail(email); + user.setPhoneNumber(phoneNumber); + if (password != null && !password.isBlank()) { + user.setPassword(hasher.hash(password)); + } + repo.updateUser(user); + } +} diff --git a/src/main/java/application/user/UserFavorite.java b/src/main/java/application/user/UserFavorite.java new file mode 100644 index 0000000..23f141f --- /dev/null +++ b/src/main/java/application/user/UserFavorite.java @@ -0,0 +1,61 @@ +package application.user; + +import domain.organization.Organization; +import persistence.dao.FavoriteDao; + + +import java.util.List; + +public class UserFavorite { + + private final FavoriteDao favoriteDao; + + /** + * Use case class for managing a user's favourite organizations. + * Delegates all persistence operations to {@link FavoriteDao}. + */ + public UserFavorite(FavoriteDao favouriteDao) { + this.favoriteDao = favouriteDao; + } + + /** + * Adds an organization to a user's favorites. + * + * @param userId the unique identifier of the user + * @param orgNumber the organization number to add as favourite + */ + public void addFavorite(long userId, String orgNumber) { + favoriteDao.addFavorite(userId, orgNumber); + } + + /** + * Removes an organization from a user's favorites. + * + * @param userId the unique identifier of the user + * @param orgNumber the organization number to remove + */ + public void removeFavorite(long userId, String orgNumber) { + favoriteDao.removeFavorite(userId, orgNumber); + } + + /** + * Retrieves all favorite organizations for a given user. + * + * @param userId the unique identifier of the user + * @return a list of {@link Organization} objects the user has saved as favourites + */ + public List getFavorites(long userId) { + return favoriteDao.getFavorites(userId); + } + + /** + * Checks whether an organization is saved as a favourite by a specific user. + * + * @param userId the unique identifier of the user + * @param orgNumber the organization number to check + * @return {@code true} if the organization is a favourite, {@code false} otherwise + */ + public boolean isFavorite(long userId, String orgNumber) { + return favoriteDao.isFavorite(userId, orgNumber); + } +} diff --git a/src/main/java/application/user/UserLogin.java b/src/main/java/application/user/UserLogin.java deleted file mode 100644 index 4392ee4..0000000 --- a/src/main/java/application/user/UserLogin.java +++ /dev/null @@ -1,36 +0,0 @@ -package application.user; - -import application.security.PasswordHasher; -import domain.user.User; -import persistence.dao.UserRepository; - -public class UserLogin { - - private final PasswordHasher hasher; - private final UserRepository repo; - - public UserLogin(PasswordHasher hasher, UserRepository repo) { - this.hasher = hasher; - this.repo = repo; - } - - public User execute(String login, String password) { - // normalize (trim etc.) - login = login.trim().toLowerCase(); - - User user = repo.findByLogin(login) - .orElseThrow(() -> new IllegalArgumentException("Invalid credentials")); - - // hash password (optional) - String hashedInput = hasher.hash(password); - - // compare hashed input with stored hash - if (!hashedInput.equals(user.getPassword())) { - throw new IllegalArgumentException("Invalid password"); - } - - System.out.println("Loggin in"); - // return user (if match) - return user; - } -} diff --git a/src/main/java/application/user/UserSignIn.java b/src/main/java/application/user/UserSignIn.java index f7ea596..7e68429 100644 --- a/src/main/java/application/user/UserSignIn.java +++ b/src/main/java/application/user/UserSignIn.java @@ -36,7 +36,7 @@ public UserSignIn(PasswordHasher hasher, UserRepository repo) { * @return user object, if it exists. */ public User execute(String login, String password) { - login.trim().toLowerCase(); + login = login.trim().toLowerCase(); User user = repo.findByLogin(login) .orElseThrow(() -> new IllegalArgumentException("Invalid credentials")); diff --git a/src/main/java/domain/donation/Donation.java b/src/main/java/domain/donation/Donation.java index 53e1063..47385b2 100644 --- a/src/main/java/domain/donation/Donation.java +++ b/src/main/java/domain/donation/Donation.java @@ -19,11 +19,12 @@ public class Donation { private final Organization organization; /** - * Donation takes in parameters that is needed to define what a donation is. - * Requires both user and organization to be not null. - * @param amount - * @param user - * @param organization + * Creates a new donation with the current date and time. + * + * @param amount the donation amount, must be greater than 0 + * @param user the user making the donation, cannot be null + * @param organization the organization receiving the donation, cannot be null + * @throws IllegalArgumentException if amount is zero or negative */ public Donation(BigDecimal amount, User user, Organization organization) { Objects.requireNonNull(amount, "Amount cannot be null"); @@ -37,6 +38,26 @@ public Donation(BigDecimal amount, User user, Organization organization) { this.organization = Objects.requireNonNull(organization, "Organization cannot be null"); } + /** + * Creates a donation with an existing date and time, used when loading from the database. + * + * @param amount the donation amount, must be greater than 0 + * @param user the user making the donation, cannot be null + * @param organization the organization receiving the donation, cannot be null + * @param dateTime the date and time of the donation, cannot be null + * @throws IllegalArgumentException if amount is zero or negative + */ + public Donation(BigDecimal amount, User user, Organization organization, LocalDateTime dateTime) { + Objects.requireNonNull(amount, "Amount cannot be null"); + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Amount must be greater than 0"); + } + this.amount = amount; + this.dateTime = Objects.requireNonNull(dateTime, "DateTime cannot be null"); + this.user = Objects.requireNonNull(user, "User cannot be null"); + this.organization = Objects.requireNonNull(organization, "Organization cannot be null"); + } + /** * Sets the database-generated ID for this donation. diff --git a/src/main/java/persistence/dao/DonationDao.java b/src/main/java/persistence/dao/DonationDao.java index dcf6cb1..a1f07ff 100644 --- a/src/main/java/persistence/dao/DonationDao.java +++ b/src/main/java/persistence/dao/DonationDao.java @@ -9,6 +9,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import persistence.db.Database; @@ -28,10 +29,6 @@ public class DonationDao { * @throws RuntimeException if a database error occurs during the insert */ public void insert(Donation donation) { - System.out.println("Inserting donation:"); - System.out.println(" user_id = " + donation.getUser().getID()); - System.out.println(" org_id = " + donation.getOrganization().getOrgNumber()); - String sql = "INSERT INTO donation(amount, donation_date" + ", user_id, organization_id) VALUES(?, ?, ?, ?)"; @@ -80,10 +77,12 @@ private Donation mapRow(ResultSet rs) throws SQLException { org.setPreApproved(rs.getInt("is_pre_approved") == 1); + Donation donation = new Donation( new BigDecimal(rs.getString("amount")), user, - org + org, + LocalDateTime.parse(rs.getString("donation_date")) ); donation.setId(rs.getLong("id")); return donation; diff --git a/src/main/java/persistence/dao/FavoriteDao.java b/src/main/java/persistence/dao/FavoriteDao.java new file mode 100644 index 0000000..57a862a --- /dev/null +++ b/src/main/java/persistence/dao/FavoriteDao.java @@ -0,0 +1,119 @@ +package persistence.dao; + +import domain.organization.Organization; +import persistence.db.Database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Data Access Object for favorite organization-related database operations. + * Provides methods for adding, removing, retrieving and checking + * a user's saved favorite organizations. + */ +public class FavoriteDao { + + + /** + * Inserts a new favorite into the database. + * Links a user to an organization they have saved as a favourite. + * + * @param userId the unique identifier of the user + * @param orgNumber the organization number of the favourite organization + * @throws RuntimeException if a database error occurs during the insert + */ + public void addFavorite(long userId, String orgNumber) { + String sql = "INSERT INTO favourite(user_id, organization_id) VALUES(?, ?)"; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setLong(1, userId); + stmt.setString(2, orgNumber); + stmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Removes a favorite from the database. + * + * @param userId the unique identifier of the user + * @param orgNumber the organization number of the favourite organization + * @throws RuntimeException if a database error occurs during the insert + */ + public void removeFavorite(long userId, String orgNumber) { + String sql = "DELETE FROM favourite WHERE user_id = ? AND organization_id = ?"; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setLong(1, userId); + stmt.setString(2, orgNumber); + stmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Retrieves all favorite organizations for a specific user. + * + * @param userId the unique identifier of the user + * @return a list of {@link Organization} objects the user has saved as favourites; + * empty list if none found + * @throws RuntimeException if a database error occurs during the query + */ + public List getFavorites(long userId) { + String sql = """ + SELECT o.* FROM favourite f + JOIN organization o ON f.organization_id = o.org_number + WHERE f.user_id = ?"""; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setLong(1, userId); + ResultSet rs = stmt.executeQuery(); + List list = new ArrayList<>(); + + while (rs.next()) { + Organization org = new Organization(); + org.setOrgNumber(rs.getString("org_number")); + org.setName(rs.getString("name")); + org.setStatus(rs.getString("status")); + org.setUrl(rs.getString("url")); + org.setPreApproved(rs.getInt("is_pre_approved") == 1); + list.add(org); + } + return list; + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Checks whether an organization is saved as a favorite by a specific user. + * + * @param userId the unique identifier of the user + * @param orgNumber the organization number to check + * @return {@code true} if the organization is a favourite, {@code false} otherwise + * @throws RuntimeException if a database error occurs during the query + */ + public boolean isFavorite(long userId, String orgNumber) { + String sql = "SELECT COUNT(*) FROM favourite WHERE user_id = ? AND organization_id = ?"; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setLong(1, userId); + stmt.setString(2, orgNumber); + ResultSet rs = stmt.executeQuery(); + return rs.next() && rs.getInt(1) > 0; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/persistence/dao/UserDao.java b/src/main/java/persistence/dao/UserDao.java index a1b0544..5283890 100644 --- a/src/main/java/persistence/dao/UserDao.java +++ b/src/main/java/persistence/dao/UserDao.java @@ -204,4 +204,25 @@ private User mapUser(ResultSet rs) throws SQLException { return user; } + /** + * Updates an existing user's profile information in the database. + * + * @param user the {@link User} containing the updated information to persist + * @throws RuntimeException if a database error occurs during the update + */ + public void updateUser(User user) { + String sql = "UPDATE user SET user_name = ?, phone_number = ?, e_mail = ?, password_hash = ? WHERE id = ?"; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + stmt.setString(1, user.getUsername()); + stmt.setString(2, user.getPhoneNumber()); + stmt.setString(3, user.getEmail()); + stmt.setString(4, user.getPassword()); + stmt.setLong(5, user.getID()); + stmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/persistence/dao/UserRepository.java b/src/main/java/persistence/dao/UserRepository.java index 0ee64c6..c0afda4 100644 --- a/src/main/java/persistence/dao/UserRepository.java +++ b/src/main/java/persistence/dao/UserRepository.java @@ -15,4 +15,5 @@ public interface UserRepository { boolean existsByEmail(String email); boolean existsByUsername(String username); void insert(User user); + void updateUser(User user); } diff --git a/src/main/java/ui/Page.java b/src/main/java/ui/Page.java index 77cac0f..4a3597b 100644 --- a/src/main/java/ui/Page.java +++ b/src/main/java/ui/Page.java @@ -6,15 +6,15 @@ */ public enum Page { HOME("Home.fxml"), - PROFILE("MyProfile.fxml"), - SIGN_IN("SignIn.fxml"), - REGISTER("Register.fxml"), - ORGANIZATIONS("OrganizationsView.fxml"), - MY_PROFILE("MyProfile.fxml"), - DONATION_NOT_LOGGED_IN("DonationNotLoggedIn.fxml"), - DONATION_AMOUNT("DonationAmount.fxml"), - DONATION_PAYMENT("DonationPayment.fxml"), - DONATION_CONFIRMATION("DonationConfirmation.fxml"); + PROFILE("profileView/MyProfile.fxml"), + SIGN_IN("auth/SignIn.fxml"), + REGISTER("auth/Register.fxml"), + ORGANIZATIONS("organizationView/OrganizationsView.fxml"), + MY_PROFILE("profileView/MyProfile.fxml"), + DONATION_NOT_LOGGED_IN("donationView/DonationNotLoggedIn.fxml"), + DONATION_AMOUNT("donationView/DonationAmount.fxml"), + DONATION_PAYMENT("donationView/DonationPayment.fxml"), + DONATION_CONFIRMATION("donationView/DonationConfirmation.fxml"); private final String fileName; diff --git a/src/main/java/ui/controller/MainController.java b/src/main/java/ui/controller/MainController.java index c586dbf..40d5414 100644 --- a/src/main/java/ui/controller/MainController.java +++ b/src/main/java/ui/controller/MainController.java @@ -9,13 +9,21 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; +import javafx.scene.control.Button; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import persistence.dao.UserDao; import ui.DonationSession; import ui.Page; import ui.DonationSessionAware; +import ui.controller.auth.RegisterController; +import ui.controller.auth.SignInController; import ui.controller.donation.DonationFlowController; +import ui.controller.organization.OrganizationController; +import ui.controller.organization.OrganizationDetailController; +import ui.controller.organization.SignInRequiredController; +import ui.controller.profile.MyProfileController; + import java.io.IOException; /** @@ -36,6 +44,10 @@ public class MainController { private DonationSession donationSession; private DonationFlowController donationFlowController; + /** + * Initializes the main controller by setting up the donation flow + * and navbar navigation callbacks, then loads the home page. + */ public void initialize() { donationFlowController = new DonationFlowController(mainPane, navbarController); donationFlowController.setOnNavigate(page -> { @@ -92,7 +104,14 @@ public void loadPage(Page page) throws IOException { try { loadDonationPage(org); } catch (IOException e) { throw new RuntimeException(e); } }); - c.setOnShowDetail(org -> showOrganizationDetail(org)); + c.setOnShowDetail((org, heartBtn) -> showOrganizationDetail(org, heartBtn)); + c.setOnSignInRequired(() -> showSignInRequired()); + } + if (controller instanceof MyProfileController c) { + c.setOnDonate(org -> { + try { loadDonationPage(org); } + catch (IOException e) { throw new RuntimeException(e); } + }); } mainPane.setCenter(root); @@ -114,11 +133,12 @@ public void loadDonationPage(Organization org) throws IOException { * Shows the organization detail popup for the given organization. * Fetches description and logo from Innsamlingskontrollen and displays them. * @param org the organization to show details for + * @param btn the heart button on the explore card, updated when favourite status changes */ - public void showOrganizationDetail(Organization org) { + public void showOrganizationDetail(Organization org, Button btn) { try { FXMLLoader loader = new FXMLLoader( - getClass().getResource("/view/OrganizationDetail.fxml")); + getClass().getResource("/view/organizationView/OrganizationDetail.fxml")); Parent root = loader.load(); OrganizationDetails details = @@ -141,6 +161,54 @@ public void showOrganizationDetail(Organization org) { throw new RuntimeException(e); } }); + controller.setOnSignIn(() -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + try { loadPage(Page.SIGN_IN); } + catch (IOException e) { throw new RuntimeException(e); } + }); + controller.setOnFavoriteChanged(isFav -> { + btn.setText(isFav ? "♥" : "♡"); + }); + + overlay.getChildren().clear(); + overlay.getChildren().add(root); + overlay.setVisible(true); + overlay.setManaged(true); + overlay.setStyle("-fx-background-color: rgba(0,0,0,0.5);"); + overlay.setMaxWidth(Double.MAX_VALUE); + overlay.setMaxHeight(Double.MAX_VALUE); + overlay.setAlignment(javafx.geometry.Pos.CENTER); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Shows the sign in required popup. + * Displayed when a non-authenticated user tries to save an organization. + */ + public void showSignInRequired() { + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/view/organizationView/SignInRequired.fxml")); + Parent root = loader.load(); + + SignInRequiredController controller = loader.getController(); + controller.setOnClose(() -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + }); + controller.setOnSignIn(() -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + try { loadPage(Page.SIGN_IN); } + catch (IOException e) { throw new RuntimeException(e); } + }); overlay.getChildren().clear(); overlay.getChildren().add(root); diff --git a/src/main/java/ui/controller/MyProfileController.java b/src/main/java/ui/controller/MyProfileController.java deleted file mode 100644 index b8dd62c..0000000 --- a/src/main/java/ui/controller/MyProfileController.java +++ /dev/null @@ -1,81 +0,0 @@ -package ui.controller; - -import application.user.UserStatistics; -import domain.donation.Donation; -import domain.user.User; -import java.util.List; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import persistence.dao.DonationDao; -import ui.Page; -import util.SessionManager; - -import java.util.function.Consumer; - -/** - * Controller for the user profile page. - * Displays user information, donation statistics and donation history. - */ -public class MyProfileController implements NavigationAware { - - private Consumer onNavigate; - @FXML private Label totalDonatedLabel; - @FXML private Label numberOfDonationsLabel; - @FXML private Label favoriteOrgLabel; - @FXML private Label nameLabel; - @FXML private Label emailLabel; - @FXML private Label phoneLabel; - @FXML private TableView donationTable; - @FXML private TableColumn orgColumn; - @FXML private TableColumn amountColumn; - @FXML private TableColumn dateColumn; - - /** - * Sets the navigation callback. - * @param onNavigate the callback for navigating to a page - */ - public void setOnNavigate(Consumer onNavigate) { - this.onNavigate = onNavigate; - } - - @FXML - public void initialize() { - User user = SessionManager.getSignedInUser(); - if (user == null) return; - - nameLabel.setText("Name: " + user.getUsername()); - emailLabel.setText("E-mail: " + user.getEmail()); - phoneLabel.setText("Phonenumber: " + user.getPhoneNumber()); - - UserStatistics stats = new UserStatistics(); - totalDonatedLabel.setText(stats.userTotalDonationAmount(user) + " kr"); - numberOfDonationsLabel.setText(stats.getTotalDonationsMade(user)); - - List favOrg = stats.userFavoriteOrganization(user); - favoriteOrgLabel.setText(favOrg.isEmpty() ? "None" : favOrg.get(0)); - - orgColumn.setCellValueFactory(cell -> - new SimpleStringProperty(cell.getValue().getOrganization().getName())); - amountColumn.setCellValueFactory(cell -> - new SimpleStringProperty(cell.getValue().getAmount().toString() + " kr")); - dateColumn.setCellValueFactory(cell -> - new SimpleStringProperty(cell.getValue().getDateTime().toLocalDate().toString())); - - DonationDao donationDao = new DonationDao(); - ObservableList data = FXCollections.observableArrayList( - donationDao.findByUser(user.getID()) - ); - donationTable.setItems(data); - } - - @FXML - private void handleSignOut() { - SessionManager.signOut(); - onNavigate.accept(Page.HOME); - } -} diff --git a/src/main/java/ui/controller/NavbarController.java b/src/main/java/ui/controller/NavbarController.java index af0d65f..3c070e2 100644 --- a/src/main/java/ui/controller/NavbarController.java +++ b/src/main/java/ui/controller/NavbarController.java @@ -65,7 +65,7 @@ public void goHome() { } /** - * Navigates to the sign in page or profile page depending on authentication state. + * Navigates to the sign-in page or profile page depending on authentication state. */ @FXML public void goToSignIn() { diff --git a/src/main/java/ui/controller/RegisterController.java b/src/main/java/ui/controller/auth/RegisterController.java similarity index 97% rename from src/main/java/ui/controller/RegisterController.java rename to src/main/java/ui/controller/auth/RegisterController.java index db662f8..7f480dc 100644 --- a/src/main/java/ui/controller/RegisterController.java +++ b/src/main/java/ui/controller/auth/RegisterController.java @@ -1,4 +1,4 @@ -package ui.controller; +package ui.controller.auth; import application.user.UserRegister; import javafx.fxml.FXML; @@ -6,6 +6,7 @@ import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import ui.Page; +import ui.controller.NavigationAware; import java.util.function.Consumer; diff --git a/src/main/java/ui/controller/SignInController.java b/src/main/java/ui/controller/auth/SignInController.java similarity index 96% rename from src/main/java/ui/controller/SignInController.java rename to src/main/java/ui/controller/auth/SignInController.java index 6cde88c..d715703 100644 --- a/src/main/java/ui/controller/SignInController.java +++ b/src/main/java/ui/controller/auth/SignInController.java @@ -1,4 +1,4 @@ -package ui.controller; +package ui.controller.auth; import application.user.UserSignIn; import domain.user.User; @@ -7,6 +7,7 @@ import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import ui.Page; +import ui.controller.NavigationAware; import util.SessionManager; import java.io.IOException; import java.util.function.Consumer; diff --git a/src/main/java/ui/controller/donation/DonationFlowController.java b/src/main/java/ui/controller/donation/DonationFlowController.java index 6b70027..05f7d7e 100644 --- a/src/main/java/ui/controller/donation/DonationFlowController.java +++ b/src/main/java/ui/controller/donation/DonationFlowController.java @@ -55,7 +55,7 @@ public void start(Organization org) throws IOException { // Loads the "not logged in" page private void loadStep1() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationNotLoggedIn.fxml")); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/donationView/DonationNotLoggedIn.fxml")); Parent root = loader.load(); DonationNotLoggedInController controller = loader.getController(); @@ -67,7 +67,7 @@ private void loadStep1() throws IOException { // Loads the amount selection page private void loadStep2() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationAmount.fxml")); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/donationView/DonationAmount.fxml")); Parent root = loader.load(); DonationAmountController controller = loader.getController(); @@ -92,7 +92,7 @@ private void loadStep2() throws IOException { // Loads the payment information page private void loadStep3() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationPayment.fxml")); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/donationView/DonationPayment.fxml")); Parent root = loader.load(); DonationPaymentController controller = loader.getController(); @@ -123,7 +123,7 @@ private void loadStep3() throws IOException { // Loads the confirmation page private void loadStep4() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationConfirmation.fxml")); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/donationView/DonationConfirmation.fxml")); Parent root = loader.load(); DonationConfirmationController controller = loader.getController(); diff --git a/src/main/java/ui/controller/OrganizationController.java b/src/main/java/ui/controller/organization/OrganizationController.java similarity index 64% rename from src/main/java/ui/controller/OrganizationController.java rename to src/main/java/ui/controller/organization/OrganizationController.java index 00ad56c..782b1f1 100644 --- a/src/main/java/ui/controller/OrganizationController.java +++ b/src/main/java/ui/controller/organization/OrganizationController.java @@ -1,19 +1,27 @@ -package ui.controller; +package ui.controller.organization; +import application.user.UserFavorite; import domain.organization.Organization; +import domain.user.User; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import persistence.dao.FavoriteDao; import persistence.dao.OrganizationDao; import ui.Page; +import ui.controller.NavigationAware; +import util.SessionManager; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -21,7 +29,7 @@ * Controller for the organizations page. * Displays a paginated list of approved organizations with search functionality. */ -public class OrganizationController implements NavigationAware{ +public class OrganizationController implements NavigationAware { @FXML private TextField searchField; @@ -41,7 +49,10 @@ public class OrganizationController implements NavigationAware{ private Consumer onNavigate; private Consumer onDonate; - private Consumer onShowDetail; + private BiConsumer onShowDetail; + private Runnable onSignInRequired; + + private final UserFavorite userFavorite = new UserFavorite(new FavoriteDao()); /** * Sets the navigation callback. @@ -63,10 +74,22 @@ public void setOnDonate(Consumer onDonate) { * Sets the callback to invoke when the user clicks on an organization card. * @param onShowDetail the callback with the selected organization */ - public void setOnShowDetail(Consumer onShowDetail) { + public void setOnShowDetail(BiConsumer onShowDetail) { this.onShowDetail = onShowDetail; } + /** + * Sets the callback to invoke when an unauthenticated user attempts to save an organization. + * @param onSignInRequired the callback to run when sign in is required + */ + public void setOnSignInRequired(Runnable onSignInRequired) { + this.onSignInRequired = onSignInRequired; + } + + /** + * Initializes the controller by loading all approved organizations, + * setting up search functionality and responsive card layout. + */ public void initialize() { try { OrganizationDao dao = new OrganizationDao(); @@ -130,22 +153,56 @@ private VBox createCard(Organization org) { name.getStyleClass().add("org-name"); name.setWrapText(true); + Button heartBtn = new Button("♥"); + heartBtn.setVisible(false); + heartBtn.getStyleClass().add("heart-button"); + User user = SessionManager.getSignedInUser(); + boolean isFav = user != null && userFavorite.isFavorite(user.getID(), org.getOrgNumber()); + heartBtn.setOnAction(e -> { + if (user == null) { + if (onSignInRequired != null) onSignInRequired.run(); + return; + } + if (heartBtn.getText().equals("♡")) { + userFavorite.addFavorite(user.getID(), org.getOrgNumber()); + heartBtn.setText("♥"); + } else { + userFavorite.removeFavorite(user.getID(), org.getOrgNumber()); + heartBtn.setText("♡"); + } + }); + heartBtn.setText(isFav ? "♥" : "♡"); + + HBox topRow = new HBox(); + topRow.getChildren().addAll(name, heartBtn); + name.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(name, Priority.ALWAYS); + heartBtn.setMinWidth(30); + + card.setOnMouseEntered(e -> heartBtn.setVisible(true)); + card.setOnMouseExited(e -> heartBtn.setVisible(false)); + Label orgnr = new Label("Org.nr: " + org.getOrgNumber()); orgnr.getStyleClass().add("org-detail"); Label status = new Label(org.isPreApproved() ? "✓ Forhåndsgodkjent" : "✓ Verified"); status.getStyleClass().add("org-status"); + Button detailBtn = new Button("Details"); + detailBtn.setMaxWidth(Double.MAX_VALUE); + detailBtn.setOnAction(e -> onShowDetail.accept(org, heartBtn)); + detailBtn.getStyleClass().add("details-button"); + Button donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); donateBtn.setMaxWidth(Double.MAX_VALUE); donateBtn.setOnAction(e -> onDonate.accept(org)); - - card.setOnMouseClicked(e -> onShowDetail.accept(org)); - card.getChildren().addAll(name, orgnr, status, donateBtn); + card.setOnMouseClicked(e -> onShowDetail.accept(org, heartBtn)); + card.getChildren().addAll(topRow, orgnr, status, detailBtn, donateBtn); return card; } + @FXML private void previousPage() { page--; diff --git a/src/main/java/ui/controller/OrganizationDetailController.java b/src/main/java/ui/controller/organization/OrganizationDetailController.java similarity index 60% rename from src/main/java/ui/controller/OrganizationDetailController.java rename to src/main/java/ui/controller/organization/OrganizationDetailController.java index 39b2e92..a1d441f 100644 --- a/src/main/java/ui/controller/OrganizationDetailController.java +++ b/src/main/java/ui/controller/organization/OrganizationDetailController.java @@ -1,12 +1,18 @@ -package ui.controller; +package ui.controller.organization; +import application.user.UserFavorite; import domain.organization.Organization; +import domain.user.User; import integration.OrganizationDetails; import javafx.fxml.FXML; +import javafx.scene.control.Button; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import persistence.dao.FavoriteDao; +import util.SessionManager; + import java.awt.Desktop; import java.net.URI; import java.util.function.Consumer; @@ -23,11 +29,15 @@ public class OrganizationDetailController { @FXML private Label descriptionLabel; @FXML private ImageView logoImageView; @FXML private Hyperlink readMoreLink; - + @FXML private Button saveBtn; private Organization organization; + private UserFavorite userFavorite = new UserFavorite(new FavoriteDao()); private Consumer onDonate; private Runnable onClose; + private Runnable onSignIn; + private Consumer onFavoriteChanged; + /** * Sets the organization and its details, and updates all UI elements. @@ -73,8 +83,10 @@ public void setOrganization(Organization org, OrganizationDetails details) { } else { logoImageView.setVisible(false); } - System.out.println("Beskrivelse: " + details.getDescription()); - System.out.println("Lengde: " + details.getDescription().length()); + + User user = SessionManager.getSignedInUser(); + boolean isFav = user != null && userFavorite.isFavorite(user.getID(), org.getOrgNumber()); + saveBtn.setText(isFav ? " ♥\nSaved" : " ♡\nSave"); } /** @@ -93,16 +105,25 @@ public void setOnClose(Runnable onClose) { this.onClose = onClose; } + /** + * Closes the organization detail popup. + */ @FXML public void handleClose() { if (onClose != null) onClose.run(); } + /** + * Triggers the donate callback for the current organization. + */ @FXML public void handleDonate() { if (onDonate != null) onDonate.accept(organization); } + /** + * Opens the organization's website in the default browser. + */ @FXML public void handleVisitWebsite() { try { @@ -112,8 +133,49 @@ public void handleVisitWebsite() { } } + /** + * Opens the organization's website to read more about it. + */ @FXML public void handleReadMore() { handleVisitWebsite(); } + + /** + * Toggles the favorite status of the current organization for the signed-in user. + * If the user is not signed in, the sign-in callback is triggered instead. + */ + @FXML + public void handleSave() { + User user = SessionManager.getSignedInUser(); + if (user == null) { + if (onSignIn != null) onSignIn.run(); + return; + } + if (userFavorite.isFavorite(user.getID(), organization.getOrgNumber())) { + userFavorite.removeFavorite(user.getID(), organization.getOrgNumber()); + saveBtn.setText("♡\nSave"); + if (onFavoriteChanged != null) onFavoriteChanged.accept(false); + } else { + userFavorite.addFavorite(user.getID(), organization.getOrgNumber()); + saveBtn.setText("♥\nSaved"); + if (onFavoriteChanged != null) onFavoriteChanged.accept(true); + } + } + + /** + * Sets the callback to invoke when the user's favorite status changes. + * @param onFavoriteChanged the callback with {@code true} if saved, {@code false} if removed + */ + public void setOnFavoriteChanged(Consumer onFavoriteChanged) { + this.onFavoriteChanged = onFavoriteChanged; + } + + /** + * Sets the callback to invoke when sign in is required. + * @param onSignIn the callback to run when the user is not signed in + */ + public void setOnSignIn(Runnable onSignIn) { + this.onSignIn = onSignIn; + } } diff --git a/src/main/java/ui/controller/organization/SignInRequiredController.java b/src/main/java/ui/controller/organization/SignInRequiredController.java new file mode 100644 index 0000000..3dec275 --- /dev/null +++ b/src/main/java/ui/controller/organization/SignInRequiredController.java @@ -0,0 +1,45 @@ +package ui.controller.organization; + +import javafx.fxml.FXML; + +/** + * Controller for the sign in required popup. + * Shown when a non-authenticated user tries to save an organization. + */ +public class SignInRequiredController { + + private Runnable onClose; + private Runnable onSignIn; + + /** + * Sets the callback to invoke when the user closes the popup. + * @param onClose the callback to run on close + */ + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + /** + * Sets the callback to invoke when the user clicks Sign in. + * @param onSignIn the callback to run on sign in + */ + public void setOnSignIn(Runnable onSignIn) { + this.onSignIn = onSignIn; + } + + /** + * Closes the popup without navigating away. + */ + @FXML + public void handleCancel() { + if (onClose != null) onClose.run(); + } + + /** + * Closes the popup and triggers the sign in callback. + */ + @FXML + public void handleSignIn() { + if (onSignIn != null) onSignIn.run(); + } +} diff --git a/src/main/java/ui/controller/profile/MyProfileController.java b/src/main/java/ui/controller/profile/MyProfileController.java new file mode 100644 index 0000000..ba16fbf --- /dev/null +++ b/src/main/java/ui/controller/profile/MyProfileController.java @@ -0,0 +1,218 @@ +package ui.controller.profile; + +import application.user.UserEditProfile; +import application.user.UserFavorite; +import application.user.UserStatistics; +import domain.donation.Donation; +import domain.organization.Organization; +import domain.user.User; +import java.util.List; + +import integration.security.Sha256PasswordHasher; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import persistence.dao.DonationDao; +import persistence.dao.FavoriteDao; +import persistence.dao.UserDao; +import ui.Page; +import ui.controller.NavigationAware; +import util.SessionManager; + +import java.util.function.Consumer; + +/** + * Controller for the user profile page. + * Displays user information, donation statistics and donation history. + */ +public class MyProfileController implements NavigationAware { + private final UserFavorite userFavorite = new UserFavorite(new FavoriteDao()); + private Consumer onNavigate; + private Consumer onDonate; + @FXML private Label totalDonatedLabel; + @FXML private Label numberOfDonationsLabel; + @FXML private Label favoriteOrgLabel; + @FXML private Label nameLabel; + @FXML private Label emailLabel; + @FXML private Label phoneLabel; + @FXML private VBox myProfile; + @FXML private TableView donationTable; + @FXML private TableColumn orgColumn; + @FXML private TableColumn amountColumn; + @FXML private TableColumn dateColumn; + @FXML private FlowPane savedOrgsPane; + @FXML private VBox editProfilePane; + @FXML private TextField editNameField; + @FXML private TextField editEmailField; + @FXML private TextField editPhoneField; + @FXML private PasswordField newPasswordField; + @FXML private PasswordField confirmPasswordField; + private final UserEditProfile userEditProfile = new UserEditProfile(new Sha256PasswordHasher(), new UserDao()); + @FXML private Label errorLabel; + @FXML private ScrollPane scrollPane; + + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + /** + * Sets the callback to invoke when the user clicks Donate on a saved organization card. + * @param onDonate the callback with the selected organization + */ + public void setOnDonate(Consumer onDonate) { + this.onDonate = onDonate; + } + + /** + * Initializes the profile page by loading user information, donation statistics, + * donation history and saved organizations. + */ + @FXML + public void initialize() { + User user = SessionManager.getSignedInUser(); + if (user == null) return; + + nameLabel.setText(user.getUsername()); + emailLabel.setText(user.getEmail()); + phoneLabel.setText(user.getPhoneNumber()); + + UserStatistics stats = new UserStatistics(); + totalDonatedLabel.setText(stats.userTotalDonationAmount(user) + " kr"); + numberOfDonationsLabel.setText(stats.getTotalDonationsMade(user)); + + List favOrg = stats.userFavoriteOrganization(user); + favoriteOrgLabel.setText(favOrg.isEmpty() ? "None" : favOrg.getFirst()); + + orgColumn.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getOrganization().getName())); + amountColumn.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getAmount().toString() + " kr")); + dateColumn.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getDateTime().toLocalDate().toString())); + + DonationDao donationDao = new DonationDao(); + ObservableList data = FXCollections.observableArrayList( + donationDao.findByUser(user.getID()) + ); + donationTable.setItems(data); + + List favorites = userFavorite.getFavorites(user.getID()); + if (favorites.isEmpty()) { + savedOrgsPane.getChildren().add(new Label("No saved organizations yet")); + } else { + for (Organization org : favorites) { + savedOrgsPane.getChildren().add(createFavoriteCard(org)); + } + } + } + + @FXML + private void handleSignOut() { + SessionManager.signOut(); + onNavigate.accept(Page.HOME); + } + + private VBox createFavoriteCard(Organization org) { + User user = SessionManager.getSignedInUser(); + + VBox card = new VBox(8); + card.getStyleClass().add("org-card"); + + Label name = new Label(org.getName()); + name.getStyleClass().add("org-name"); + name.setMaxWidth(Double.MAX_VALUE); + + Button removeBtn = new Button("♥"); + removeBtn.getStyleClass().add("remove-button"); + removeBtn.setOnAction(e -> { + userFavorite.removeFavorite(user.getID(), org.getOrgNumber()); + savedOrgsPane.getChildren().remove(card); + }); + + HBox topRow = new HBox(15); + topRow.getChildren().addAll(name, removeBtn); + HBox.setHgrow(name, Priority.ALWAYS); + + Button donateBtn = new Button("Donate"); + donateBtn.getStyleClass().add("donate-button"); + donateBtn.setMaxWidth(Double.MAX_VALUE); + donateBtn.setOnAction(e -> onDonate.accept(org)); + + card.getChildren().addAll(topRow, donateBtn); + return card; + } + + /** + * Switches the profile view to edit mode, pre-filling fields with current user data. + */ + @FXML + public void handleEditProfile() { + User user = SessionManager.getSignedInUser(); + editNameField.setText(user.getUsername()); + editEmailField.setText(user.getEmail()); + editPhoneField.setText(user.getPhoneNumber()); + + myProfile.setVisible(false); + myProfile.setManaged(false); + editProfilePane.setVisible(true); + editProfilePane.setManaged(true); + } + + /** + * Validates and saves the updated profile information. + * Shows an error message if passwords do not match. + */ + @FXML + public void handleSaveProfile() { + String newPassword = newPasswordField.getText(); + String confirmPassword = confirmPasswordField.getText(); + + if (!newPassword.isBlank() && !newPassword.equals(confirmPassword)) { + errorLabel.setText("Passwords do not match"); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + return; + } + User user = SessionManager.getSignedInUser(); + userEditProfile.execute( + user, + editNameField.getText(), + editEmailField.getText(), + editPhoneField.getText(), + newPassword + ); + nameLabel.setText(user.getUsername()); + emailLabel.setText(user.getEmail()); + phoneLabel.setText(user.getPhoneNumber()); + handleCancelEdit(); + } + + /** + * Cancels profile editing and returns to the profile view. + */ + @FXML + public void handleCancelEdit() { + myProfile.setVisible(true); + myProfile.setManaged(true); + editProfilePane.setVisible(false); + editProfilePane.setManaged(false); + myProfile.requestFocus(); + } +} diff --git a/src/main/resources/css/Global.css b/src/main/resources/css/Global.css index e0253c6..a8ec809 100644 --- a/src/main/resources/css/Global.css +++ b/src/main/resources/css/Global.css @@ -187,3 +187,8 @@ -fx-scale-x: 0.97; -fx-scale-y: 0.97; } + +.error-label { + -fx-text-fill: red; + -fx-font-size: 16px; +} \ No newline at end of file diff --git a/src/main/resources/css/MyProfile.css b/src/main/resources/css/MyProfile.css index a9b04ec..fbed4fc 100644 --- a/src/main/resources/css/MyProfile.css +++ b/src/main/resources/css/MyProfile.css @@ -1,4 +1,32 @@ .profile-label{ -fx-font-size: 16px; +} + +.remove-button { + -fx-background-color: transparent; + -fx-font-size: 16px; + -fx-cursor: hand; + -fx-padding: 0; +} + +.table-view .column-header { + -fx-background-color: #1a2e6b; +} + +.table-view .column-header .label { + -fx-text-fill: white; + -fx-font-weight: bold; +} + +.table-view .table-row-cell:odd { + -fx-background-color: #f5f5f5; +} + +.table-view .table-row-cell:even { + -fx-background-color: white; +} + +.table-view .table-cell { + -fx-padding: 8 16 8 16; } \ No newline at end of file diff --git a/src/main/resources/css/OrganizationView.css b/src/main/resources/css/OrganizationView.css index 18c0a90..24500ca 100644 --- a/src/main/resources/css/OrganizationView.css +++ b/src/main/resources/css/OrganizationView.css @@ -10,12 +10,6 @@ -fx-cursor: hand; } -.org-card:pressed { - -fx-scale-x: 0.98; - -fx-scale-y: 0.98; - -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.05), 4, 0, 0, 2); -} - .org-name { -fx-font-size: 20px; } @@ -114,4 +108,33 @@ -fx-font-size: 13px; -fx-text-fill: #333333; -fx-wrap-text: true; +} + +.details-button { + -fx-background-color: transparent; + -fx-border-color: #1a2e6b; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-border-width: 1; + -fx-text-fill: #1a2e6b; + -fx-cursor: hand; +} + +.details-button:hover { + -fx-background-color: #1a2e6b; + -fx-text-fill: white; + -fx-scale-x: 1.02; + -fx-scale-y: 1.02; +} + +.details-button:pressed { + -fx-scale-x: 0.98; + -fx-scale-y: 0.98; +} + +.heart-button { + -fx-font-size: 24px; + -fx-background-color: transparent; + -fx-cursor: hand; + -fx-padding: 0; } \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 5288a7e..c57b78d 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -23,4 +23,12 @@ CREATE TABLE IF NOT EXISTS donation ( organization_id TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE RESTRICT, FOREIGN KEY (organization_id) REFERENCES organization(org_number) ON DELETE RESTRICT - ); \ No newline at end of file + ); + +CREATE TABLE IF NOT EXISTS favourite ( + user_id INTEGER NOT NULL, + organization_id TEXT NOT NULL, + PRIMARY KEY (user_id, organization_id), + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE , + FOREIGN KEY (organization_id) REFERENCES organization(org_number) ON DELETE CASCADE +) \ No newline at end of file diff --git a/src/main/resources/view/Register.fxml b/src/main/resources/view/auth/Register.fxml similarity index 93% rename from src/main/resources/view/Register.fxml rename to src/main/resources/view/auth/Register.fxml index de4a9d3..e9dc241 100644 --- a/src/main/resources/view/Register.fxml +++ b/src/main/resources/view/auth/Register.fxml @@ -10,7 +10,7 @@ - +
@@ -77,7 +77,7 @@ -
- - - + + +
diff --git a/src/test/java/application/user/UserLoginTest.java b/src/test/java/application/user/UserLoginTest.java deleted file mode 100644 index 20caec1..0000000 --- a/src/test/java/application/user/UserLoginTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package application.user; - -import application.security.PasswordHasher; -import domain.user.User; -import org.junit.jupiter.api.Test; -import persistence.dao.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 index a4807cd..8ed6894 100644 --- a/src/test/java/application/user/UserRegisterTest.java +++ b/src/test/java/application/user/UserRegisterTest.java @@ -2,6 +2,7 @@ import application.security.PasswordHasher; import domain.user.User; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import persistence.dao.UserRepository; @@ -11,47 +12,53 @@ public class UserRegisterTest { + + private static final String USERNAME = "testuser"; + private static final String PHONE = "12345678"; + private static final String PASSWORD = "secret"; + private static final String EMAIL = "test@example.com"; + private static final String HASHED_PASSWORD = "hashed-password"; + + private RecordingUserRepository repo; + private UserRegister userRegister; + + @BeforeEach + void setUp() { + repo = new RecordingUserRepository(); + PasswordHasher hasher = new FakePasswordHasher(HASHED_PASSWORD); + userRegister = new UserRegister(hasher, repo); + } + @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"); + userRegister.execute(USERNAME, PHONE, PASSWORD, EMAIL); 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()); + assertEquals(USERNAME, repo.insertedUser.getUsername()); + assertEquals(PHONE, repo.insertedUser.getPhoneNumber()); + assertEquals(HASHED_PASSWORD, repo.insertedUser.getPassword()); + assertEquals(EMAIL, 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 "); + userRegister.execute(" testuser ", " 12345678 ", PASSWORD, " TEST@EXAMPLE.COM "); assertNotNull(repo.insertedUser); - assertEquals("testuser", repo.insertedUser.getUsername()); - assertEquals("12345678", repo.insertedUser.getPhoneNumber()); - assertEquals("test@example.com", repo.insertedUser.getEmail()); + assertEquals(USERNAME, repo.insertedUser.getUsername()); + assertEquals(PHONE, repo.insertedUser.getPhoneNumber()); + assertEquals(EMAIL, 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") + () -> userRegister.execute(USERNAME, PHONE, PASSWORD, EMAIL) ); assertEquals("Email already in use", exception.getMessage()); @@ -60,14 +67,11 @@ void executeThrowsWhenEmailAlreadyExists() { @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") + () -> userRegister.execute(USERNAME, PHONE, PASSWORD, EMAIL) ); assertEquals("This username is taken", exception.getMessage()); @@ -76,26 +80,21 @@ void executeThrowsWhenUsernameAlreadyExists() { @Test void executeHashesPasswordBeforeSavingUser() { - RecordingUserRepository repo = new RecordingUserRepository(); - RecordingPasswordHasher hasher = new RecordingPasswordHasher("hashed-password"); - UserRegister userRegister = new UserRegister(hasher, repo); + RecordingPasswordHasher recordingHasher = new RecordingPasswordHasher(HASHED_PASSWORD); + userRegister = new UserRegister(recordingHasher, repo); - userRegister.execute("testuser", "12345678", "secret", "test@example.com"); + userRegister.execute(USERNAME, PHONE, PASSWORD, EMAIL); - assertEquals("secret", hasher.lastPasswordInput); + assertEquals(PASSWORD, recordingHasher.lastPasswordInput); assertNotNull(repo.insertedUser); - assertEquals("hashed-password", repo.insertedUser.getPassword()); + 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") + () -> userRegister.execute(USERNAME, "1234", PASSWORD, EMAIL) ); assertEquals("Fill in a phonenumber with 8 digits", exception.getMessage()); @@ -166,5 +165,8 @@ public boolean existsByUsername(String username) { public void insert(User user) { insertedUser = user; } + + @Override + public void updateUser(User user) {} } } diff --git a/src/test/java/application/user/UserSignInTest.java b/src/test/java/application/user/UserSignInTest.java new file mode 100644 index 0000000..b223da9 --- /dev/null +++ b/src/test/java/application/user/UserSignInTest.java @@ -0,0 +1,167 @@ +package application.user; + +import application.security.PasswordHasher; +import domain.user.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import persistence.dao.UserRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class UserSignInTest { + + private static final String USERNAME = "testUser"; + private static final String RAW_PASSWORD = "secret"; + private static final String HASHED_PASSWORD = "hashed-secret"; + private static final String EMAIL = "test@example.com"; + private static final String PHONE = "12345678"; + + private User user; + + @BeforeEach + void init() { + user = new User(USERNAME, PHONE, HASHED_PASSWORD, EMAIL); + } + + @Test + void executeReturnsUserWhenCredentialsAreValid() { + PasswordHasher hasher = new FakePasswordHasher(HASHED_PASSWORD); + UserRepository repo = new FakeUserRepository(user); + + UserSignIn userSignIn = new UserSignIn(hasher, repo); + + User result = userSignIn.execute(USERNAME, RAW_PASSWORD); + + assertSame(user, result); + } + + @Test + void executeThrowsWhenLoginDoesNotExist() { + PasswordHasher hasher = new FakePasswordHasher(HASHED_PASSWORD); + UserRepository repo = new FakeUserRepository(null); + + UserSignIn userSignIn = new UserSignIn(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userSignIn.execute("missing-user", RAW_PASSWORD) + ); + + assertEquals("Invalid credentials", exception.getMessage()); + } + + @Test + void executeThrowsWhenPasswordIsWrong() { + PasswordHasher hasher = new FakePasswordHasher("different-hash"); + UserRepository repo = new FakeUserRepository(user); + + UserSignIn userSignIn = new UserSignIn(hasher, repo); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userSignIn.execute(USERNAME, "wrong-password") + ); + + assertEquals("Invalid password", exception.getMessage()); + } + + @Test + void executeDoesNotHashPasswordWhenLoginDoesNotExist() { + TrackingPasswordHasher hasher = new TrackingPasswordHasher(HASHED_PASSWORD); + UserRepository repo = new FakeUserRepository(null); + + UserSignIn userSignIn = new UserSignIn(hasher, repo); + + assertThrows(IllegalArgumentException.class, + () -> userSignIn.execute("missing-user", RAW_PASSWORD)); + + assertFalse(hasher.wasCalled); + } + + @Test + void executePassesRawPasswordToHasher() { + RecordingPasswordHasher hasher = new RecordingPasswordHasher(HASHED_PASSWORD); + UserRepository repo = new FakeUserRepository(user); + + UserSignIn userSignIn = new UserSignIn(hasher, repo); + + userSignIn.execute(USERNAME, RAW_PASSWORD); + + assertEquals(RAW_PASSWORD, hasher.lastPassword); + } + + private record FakePasswordHasher(String hashToReturn) implements PasswordHasher { + + @Override + public String hash(String password) { + return hashToReturn; + } + } + + private record FakeUserRepository(User userToReturn) implements UserRepository { + + @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.ofNullable(userToReturn); + } + + @Override + public boolean existsByEmail(String email) { + return false; + } + + @Override + public boolean existsByUsername(String username) { + return false; + } + + @Override + public void insert(User user) { + } + + @Override + public void updateUser(User user) {} + } + + private static class TrackingPasswordHasher implements PasswordHasher { + private final String hashToReturn; + private boolean wasCalled; + + private TrackingPasswordHasher(String hashToReturn) { + this.hashToReturn = hashToReturn; + } + + @Override + public String hash(String password) { + wasCalled = true; + return hashToReturn; + } + } + + private static class RecordingPasswordHasher implements PasswordHasher { + private final String hashToReturn; + private String lastPassword; + + private RecordingPasswordHasher(String hashToReturn) { + this.hashToReturn = hashToReturn; + } + + @Override + public String hash(String password) { + lastPassword = password; + return hashToReturn; + } + } +} \ No newline at end of file diff --git a/src/test/java/application/user/UserStatisticsTest.java b/src/test/java/application/user/UserStatisticsTest.java index 492ac43..b8f2413 100644 --- a/src/test/java/application/user/UserStatisticsTest.java +++ b/src/test/java/application/user/UserStatisticsTest.java @@ -1,59 +1,70 @@ package application.user; import domain.user.User; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import persistence.dao.DonationDao; import java.lang.reflect.Field; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; public class UserStatisticsTest { + private static final String USERNAME = "testuser"; + private static final String PHONE = "12345678"; + private static final String PASSWORD = "password"; + private static final String EMAIL = "test@example.com"; + + + private FakeDonationDao fakeDao; + private UserStatistics statistics; + + @BeforeEach void init() throws Exception { + fakeDao = new FakeDonationDao(); + statistics = createStatisticsWithFakeDao(fakeDao); + } + + @Test - void userFavoriteOrganization_returnsListFromDonationDao() throws Exception { - FakeDonationDao fakeDao = new FakeDonationDao(); - fakeDao.favoriteOrganizationsToReturn = List.of( - "Org1 (3 donasjoner)", - "Org2 (2 donasjoner)" + void userFavoriteOrganization_returnsListFromDonationDao() { + List expected = List.of( + "Org1 (3 donations)", + "Org2 (2 donations)" ); - UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); + fakeDao.favoriteOrganizationsToReturn = expected; + User user = createUserWithId(42L); List result = statistics.userFavoriteOrganization(user); - assertEquals(List.of("Org1 (3 donasjoner)", "Org2 (2 donasjoner)"), result); - assertEquals(42L, fakeDao.lastUserIdForFavorites); + assertEquals(expected, result); + assertEquals(42L, fakeDao.lastUserIdForFavorite); } @Test - void userDonations_returnsListFromDonationDao() 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" + void userDonations_returnsListFromDonationDao() { + List expected = List.of( + "Amount: 100, Date: 2026-04-15, Organization: Org1", + "Amount: 50, Date: 2026-04-16, Organization: Org2" ); - UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); + fakeDao.userDonationsToReturn = expected; + 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(expected, result); assertEquals(7L, fakeDao.lastUserIdForDonations); } @Test - void userTotalDonationAmount_returnsValueFromDonationDao() throws Exception { - FakeDonationDao fakeDao = new FakeDonationDao(); + void userTotalDonationAmount_returnsValueFromDonationDao() { fakeDao.totalDonationAmountToReturn = "250.00"; - UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); User user = createUserWithId(15L); String result = statistics.userTotalDonationAmount(user); @@ -63,11 +74,9 @@ void userTotalDonationAmount_returnsValueFromDonationDao() throws Exception { } @Test - void getTotalDonationsMade_returnsValueFromDonationDao() throws Exception { - FakeDonationDao fakeDao = new FakeDonationDao(); + void getTotalDonationsMade_returnsValueFromDonationDao() { fakeDao.totalDonationsMadeToReturn = "5"; - UserStatistics statistics = createStatisticsWithFakeDao(fakeDao); User user = createUserWithId(99L); String result = statistics.getTotalDonationsMade(user); @@ -76,16 +85,61 @@ void getTotalDonationsMade_returnsValueFromDonationDao() throws Exception { assertEquals(99L, fakeDao.lastUserIdForTotalCount); } + @Test + void userFavoriteOrganization_withNullUser_throwsNullPointerException() { + //noinspection DataFlowIssue + assertThrows(NullPointerException.class, () -> statistics.userFavoriteOrganization(null)); + } + + @Test + void userDonations_withNullUser_throwsNullPointerException() { + //noinspection DataFlowIssue + assertThrows(NullPointerException.class, () -> statistics.userDonations(null)); + } + + @Test + void userTotalDonationAmount_withNullUser_throwsNullPointerException() { + //noinspection DataFlowIssue + assertThrows(NullPointerException.class, () -> statistics.userTotalDonationAmount(null)); + } + + @Test + void getTotalDonationsMade_withNullUser_throwsNullPointerException() { + //noinspection DataFlowIssue + assertThrows(NullPointerException.class, () -> statistics.getTotalDonationsMade(null)); + } + + @Test + void userFavoriteOrganization_withUserNullId_throwsNullPointerException() { + User user = new User(USERNAME, PHONE, PASSWORD, EMAIL); + + assertThrows(NullPointerException.class, () -> statistics.userFavoriteOrganization(user)); + } + + @Test + void constructor_initializesDonationDao() throws Exception { + Field field = UserStatistics.class.getDeclaredField("donationDao"); + field.setAccessible(true); + + UserStatistics newStatistics = new UserStatistics(); + Object dao = field.get(newStatistics); + + assertNotNull(dao); + assertEquals(DonationDao.class, dao.getClass()); + } + 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 user = new User(USERNAME, PHONE, PASSWORD, EMAIL); user.setId(id); return user; } @@ -96,14 +150,14 @@ private static class FakeDonationDao extends DonationDao { String totalDonationAmountToReturn = "0"; String totalDonationsMadeToReturn = "0"; - long lastUserIdForFavorites; - long lastUserIdForDonations; - long lastUserIdForTotalAmount; - long lastUserIdForTotalCount; + private long lastUserIdForFavorite; + private long lastUserIdForDonations; + private long lastUserIdForTotalAmount; + private long lastUserIdForTotalCount; @Override public List getFavoriteOrganization(long userId) { - lastUserIdForFavorites = userId; + lastUserIdForFavorite = userId; return favoriteOrganizationsToReturn; } diff --git a/src/test/java/domain/DonationTest.java b/src/test/java/domain/DonationTest.java index 57c5be1..aafacfd 100644 --- a/src/test/java/domain/DonationTest.java +++ b/src/test/java/domain/DonationTest.java @@ -10,15 +10,17 @@ import java.time.Duration; import java.time.LocalDateTime; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; + class DonationTest { + + private static final BigDecimal VALID_AMOUNT = new BigDecimal("1500"); + private static final BigDecimal SMALL_VALID_AMOUNT = new BigDecimal("0.01"); + private User testUser; private Organization testOrganization; - private BigDecimal testAmount = new BigDecimal("1500"); @BeforeEach public void init() { @@ -30,54 +32,148 @@ public void init() { testOrganization.setUrl("https://test.com"); } + @Test - public void validDonation_createsSuccessfully() { - Donation donation = new Donation(testAmount, testUser, testOrganization); + public void constructor_withValidInput_createsDonation() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + assertNotNull(donation); } @Test - public void nullUser_throwsNullPointerException() { - NullPointerException exception = assertThrows(NullPointerException.class, - () -> new Donation(testAmount, null, testOrganization)); - assertEquals("User cannot be null", exception.getMessage()); + public void constructor_withNullUser_throwsNullPointerException() { + testUser = null; + NullPointerException Exception = assertThrows(NullPointerException.class, + () -> new Donation(VALID_AMOUNT, testUser, testOrganization)); + + assertEquals("User cannot be null", Exception.getMessage()); } @Test - public void nullOrganization_throwsNullPointerException() { - NullPointerException exception = assertThrows(NullPointerException.class, - () -> new Donation(testAmount, testUser, null)); - assertEquals("Organization cannot be null", exception.getMessage()); + public void constructor_withNullOrganization_throwsNullPointerException() { + testOrganization = null; + NullPointerException Exception = assertThrows(NullPointerException.class, + () -> new Donation(VALID_AMOUNT, testUser, testOrganization)); + + assertEquals("Organization cannot be null", Exception.getMessage()); } @Test - public void negativeAmount_throwsIllegalArgumentException() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new Donation(new BigDecimal("-23"), testUser, testOrganization)); - assertEquals("Amount must be greater than 0", exception.getMessage()); + public void constructor_withPositiveDecimalAmount_setsFieldsCorrectly() { + Donation donation = new Donation(SMALL_VALID_AMOUNT, testUser, testOrganization); + + assertEquals(SMALL_VALID_AMOUNT, donation.getAmount()); + assertEquals(testUser, donation.getUser()); + assertEquals(testOrganization, donation.getOrganization()); + } + + @Test + public void constructor_withNegativeAmount_throwsIllegalArgumentException() { + IllegalArgumentException Exception = assertThrows(IllegalArgumentException.class, + () -> new Donation(new BigDecimal("-23"), testUser, testOrganization)); + + assertEquals("Amount must be greater than 0", Exception.getMessage()); + } + + + @Test + public void constructor_withZeroAmount_throwsIllegalArgumentException() { + IllegalArgumentException Exception = assertThrows(IllegalArgumentException.class, + () -> new Donation(new BigDecimal("0"), testUser, testOrganization)); + + assertEquals("Amount must be greater than 0", Exception.getMessage()); + } + + @Test + public void constructor_withNullAmount_throwsNullPointerException() { + NullPointerException Exception = assertThrows(NullPointerException.class, + () -> new Donation(null, testUser, testOrganization)); + + assertEquals("Amount cannot be null", Exception.getMessage()); } @Test public void getAmount_returnsCorrectValue() { - Donation donation = new Donation(testAmount, testUser, testOrganization); - assertEquals(testAmount, donation.getAmount()); + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + assertEquals(VALID_AMOUNT, donation.getAmount()); + } @Test public void getDateTime_returnsCurrentTime() { LocalDateTime now = LocalDateTime.now(); - Donation donation = new Donation(testAmount, testUser, testOrganization); + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + assertTrue(Duration.between(now, donation.getDateTime()).abs().toMillis() < 1000); } @Test public void getUser_returnsCorrectUser() { - Donation donation = new Donation(testAmount, testUser, testOrganization); + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); assertEquals(testUser, donation.getUser()); } @Test public void getOrganization_returnsCorrectOrganization() { - Donation donation = new Donation(testAmount, testUser, testOrganization); + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); assertEquals(testOrganization, donation.getOrganization()); } + + @Test + public void setId_withValidPositiveId_setsIdSuccessfully() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + + donation.setId(1L); + + assertEquals(1L, donation.getId()); + + } + + @Test + public void setId_withInvalidNullId_throwsIllegalArgumentException() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> donation.setId(null)); + + assertEquals("ID must be positive", exception.getMessage()); + } + + @Test + public void setId_withInvalidNegativeId_throwsIllegalArgumentException() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> donation.setId(-1L)); + + assertEquals("ID must be positive", exception.getMessage()); + } + + @Test + public void setId_withInvalidZeroId_throwsIllegalArgumentException() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> donation.setId(0L)); + + assertEquals("ID must be positive", exception.getMessage()); + } + + @Test + public void setId_whenCalledTwice_throwsIllegalStateException() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + + donation.setId(1L); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> donation.setId(2L)); + + assertEquals("ID is already set", exception.getMessage()); + } + + @Test + public void getId_beforeSetId_throwsNullPointerException() { + Donation donation = new Donation(VALID_AMOUNT, testUser, testOrganization); + + assertThrows(NullPointerException.class, donation::getId); + } } \ No newline at end of file diff --git a/src/test/java/domain/OrganizationTest.java b/src/test/java/domain/OrganizationTest.java index b15d2da..f616b8d 100644 --- a/src/test/java/domain/OrganizationTest.java +++ b/src/test/java/domain/OrganizationTest.java @@ -1,26 +1,137 @@ package domain; +import com.fasterxml.jackson.databind.ObjectMapper; import domain.organization.Organization; import domain.user.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class OrganizationTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + void constructor_setsDefaultEmptyValues() { + Organization org = new Organization(); + + assertNull(org.getOrgNumber()); + assertNull(org.getName()); + assertNull(org.getUrl()); + assertNull(org.getStatus()); + assertFalse(org.isPreApproved()); + } + + @Test + void setters_updateAllFields() { + Organization org = new Organization(); + + org.setOrgNumber("123456789"); + org.setName("TestOrg"); + org.setUrl("https://testOrg.com"); + org.setStatus("ACTIVE"); + org.setPreApproved(true); + + assertEquals("123456789", org.getOrgNumber()); + assertEquals("TestOrg", org.getName()); + assertEquals("https://testOrg.com", org.getUrl()); + assertEquals("ACTIVE", org.getStatus()); + assertTrue(org.isPreApproved()); + } + + @Test + void deserializeJson_mapsAllFieldsCorrectly() throws Exception { + String json = """ + { + "org_number": "123456789", + "name": "TestOrg", + "status": "ACTIVE", + "url": "https://testOrg.com", + "is_pre_approved": true + } + """; + + + Organization org = mapper.readValue(json, Organization.class); + + assertEquals("123456789", org.getOrgNumber()); + assertEquals("TestOrg", org.getName()); + assertEquals("ACTIVE", org.getStatus()); + assertEquals("https://testOrg.com", org.getUrl()); + assertTrue(org.isPreApproved()); + } + private Organization organization; + @Test + void deserializeJson_ignoresUnknownFields() throws Exception { + String json = """ + { + "org_number": "123456789", + "name": "TestOrg", + "status": "ACTIVE", + "url": "https://testOrg.com", + "is_pre_approved": true, + "unknown_field": "Ignore this" + } + """; - @BeforeEach - public void init() { + Organization org = mapper.readValue(json, Organization.class); + + assertEquals("123456789", org.getOrgNumber()); + assertEquals("TestOrg", org.getName()); + assertEquals("ACTIVE", org.getStatus()); + assertEquals("https://testOrg.com", org.getUrl()); + assertTrue(org.isPreApproved()); + } + + @Test + void deserializeJson_usesDefaultValuesForMissingFields() throws Exception { + String json = """ + { + "org_number": "123456789", + "name": "TestOrg", + "status": "ACTIVE", + "url": "https://testOrg.com" + } + """; + + + Organization org = mapper.readValue(json, Organization.class); + + assertEquals("123456789", org.getOrgNumber()); + assertEquals("TestOrg", org.getName()); + assertEquals("ACTIVE", org.getStatus()); + assertEquals("https://testOrg.com", org.getUrl()); + assertFalse(org.isPreApproved()); + } + + @BeforeEach + public void init() { organization = new Organization(); organization.setOrgNumber("123456789"); organization.setName("Test Organization"); organization.setStatus("approved"); organization.setUrl("https://test.com"); organization.setPreApproved(true); + + assertEquals("123456789", organization.getOrgNumber()); + assertEquals("TestOrg", organization.getName()); + assertEquals("ACTIVE", organization.getStatus()); + assertEquals("https://testOrg.com", organization.getUrl()); + assertTrue(organization.isPreApproved()); + } + + + @Test + void setPreApproved_togglesValue() { + Organization org = new Organization(); + + org.setPreApproved(true); + assertTrue(org.isPreApproved()); + + org.setPreApproved(false); + assertFalse(org.isPreApproved()); } @Test diff --git a/src/test/java/domain/UserTest.java b/src/test/java/domain/UserTest.java index 2aa440c..5ae4d3d 100644 --- a/src/test/java/domain/UserTest.java +++ b/src/test/java/domain/UserTest.java @@ -2,141 +2,137 @@ import domain.user.User; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; + +import static org.junit.jupiter.api.Assertions.*; class UserTest { - private String testUserName = "testUser"; - private String testPhoneNumber = "90909090"; - private String testEmail = "test@email.com"; - private final String testPassword = "password"; + private static final String USERNAME = "testUser"; + private static final String PHONE = "90909090"; + private static final String EMAIL = "test@email.com"; + private static final String PASSWORD = "password"; - //Positive tests @Test - public void userTestPositive(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); + public void constructor_withValidInput_createsUser(){ + User user = createUser(); + assertNotNull(user); } - //Negative tests @Test - public void userTestUserNameBlank(){ - testUserName = ""; + public void constructor_withBlankUsername_throwsIllegalArgumentException(){ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User("", PHONE, PASSWORD, EMAIL)); assertEquals("Username has to be filled in", exception.getMessage()); } @Test - public void userTestUserNameNull(){ - testUserName = null; + public void constructor_withNullUsername_throwsIllegalArgumentException(){ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User(null, PHONE, PASSWORD, EMAIL)); assertEquals("Username has to be filled in", exception.getMessage()); } @Test - public void userTestPhoneNumberBlank(){ - testPhoneNumber = ""; + public void constructor_withBlankPhoneNumber_throwsIllegalArgumentException(){ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User(USERNAME, "", PASSWORD, EMAIL)); assertEquals("Phonenumber has to be filled in", exception.getMessage()); } @Test - public void userTestPhoneNumberNull(){ - testPhoneNumber = null; + public void constructor_withNullPhoneNumber_throwsIllegalArgumentException(){ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User(USERNAME, null, PASSWORD, EMAIL)); assertEquals("Phonenumber has to be filled in", exception.getMessage()); } @Test - public void userTestPhoneNumberShort() { - testPhoneNumber = "909090"; + public void constructor_withShortPhoneNumber_throwsIllegalArgumentException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User(USERNAME, "909090", PASSWORD, EMAIL)); assertEquals("Fill in a phonenumber with 8 digits", exception.getMessage()); } @Test - public void userTestEmailBlank(){ - testEmail = ""; + public void constructor_withBlankEmail_throwsIllegalArgumentException(){ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User(USERNAME, PHONE, PASSWORD, "")); assertEquals("Invalid email address: ", exception.getMessage()); } @Test - public void userTestEmailNull(){ - testEmail = null; + public void constructor_withNullEmail_throwsIllegalArgumentException(){ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new User(testUserName, testPhoneNumber, testPassword, testEmail)); + () -> new User(USERNAME, PHONE, PASSWORD, null)); assertEquals("Invalid email address: null", exception.getMessage()); } - //Getter tests @Test - public void userTestUserNameGet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); - assertEquals(User.getUsername(), testUserName); + public void getUsername_returnsUsername(){ + User user = createUser(); + assertEquals(USERNAME, user.getUsername()); } @Test - public void userTestPhoneNumberGet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); - assertEquals(User.getPhoneNumber(), testPhoneNumber); + public void getPhoneNumber_returnsPhoneNumber(){ + User user = createUser(); + assertEquals(PHONE, user.getPhoneNumber()); } @Test - public void userTestEmailGet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); - assertEquals(User.getEmail(), testEmail); + public void getEmail_returnsEmail(){ + User user = createUser(); + assertEquals(EMAIL, user.getEmail()); } @Test - public void userTestPasswordGet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); - assertEquals(User.getPassword(), testPassword); + public void getPassword_returnsPassword(){ + User user = createUser(); + assertEquals(PASSWORD, user.getPassword()); } - //Setter tests @Test - public void userTestPasswordSet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); - String newPassword = "NewPassoword"; - User.setPassword(newPassword); - assertEquals(newPassword, User.getPassword()); + public void setPassword_updatesPassword(){ + User user = createUser(); + String newPassword = "newPassword"; + user.setPassword(newPassword); + assertEquals(newPassword, user.getPassword()); } @Test - public void userTestUserNameSet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); + public void setUsername_updatesUsername(){ + User user = createUser(); String newUserName = "NewUsername"; - User.setUsername(newUserName); - assertEquals(newUserName, User.getUsername()); + user.setUsername(newUserName); + assertEquals(newUserName, user.getUsername()); } @Test - public void userTestPhoneNumberSet(){ - User User = new User(testUserName, testPhoneNumber, testPassword, testEmail); - String newPhoneNumber = "12345678"; - User.setPhoneNumber(newPhoneNumber); - assertEquals(newPhoneNumber, User.getPhoneNumber()); + public void setPhoneNumber_updatesPhoneNumber(){ + User user = createUser(); + String newPhoneNumber = "123456asdsad7"; + user.setPhoneNumber(newPhoneNumber); + assertEquals(newPhoneNumber, user.getPhoneNumber()); } - @Test - public void userTestEmailSet() { - User user = new User(testUserName, testPhoneNumber, testPassword, testEmail); - String newEmail = "new@email.com"; - user.setEmail(newEmail); - assertEquals(newEmail, user.getEmail()); + public void userTestEmailSet() { + User user = createUser() + String newEmail = "new@email.com"; + user.setEmail(newEmail); + assertEquals(newEmail, user.getEmail()); + } + + private User createUser() { + return new User(USERNAME, PHONE, PASSWORD, EMAIL); } + + + } \ No newline at end of file