From 9aff5fa26214f80a9a8fc11293dfc2bc91737d8c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 13:55:34 +0200 Subject: [PATCH 1/4] Update[JUnit]: add more JUnit tests for better test coverage --- .../java/edu/group5/app/model/AppState.java | 6 +- .../edu/group5/app/model/AppStateTest.java | 69 +++++++++++++++++++ .../model/donation/DonationServiceTest.java | 29 +++++++- .../app/model/donation/DonationTest.java | 12 +++- .../OrganizationRepositoryTest.java | 24 +++++++ .../organization/OrganizationScraperTest.java | 13 ++++ 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/test/java/edu/group5/app/model/AppStateTest.java diff --git a/src/main/java/edu/group5/app/model/AppState.java b/src/main/java/edu/group5/app/model/AppState.java index bd56ba0..e17bda9 100644 --- a/src/main/java/edu/group5/app/model/AppState.java +++ b/src/main/java/edu/group5/app/model/AppState.java @@ -18,7 +18,7 @@ public class AppState { private User currentUser; private BigDecimal currentDonationAmount; private Organization currentOrganization; - private String currentDonation; + private String currentPaymentMethod; /** * Gets the current user of the application. @@ -73,7 +73,7 @@ public void setCurrentDonationAmount(BigDecimal amount) { * @return the current payment method */ public String getCurrentPaymentMethod() { - return this.currentDonation; + return this.currentPaymentMethod; } /** @@ -81,6 +81,6 @@ public String getCurrentPaymentMethod() { * @param paymentMethod the payment method to set as the current payment method */ public void setCurrentPaymentMethod(String paymentMethod){ - this.currentDonation = paymentMethod; + this.currentPaymentMethod = paymentMethod; } } diff --git a/src/test/java/edu/group5/app/model/AppStateTest.java b/src/test/java/edu/group5/app/model/AppStateTest.java new file mode 100644 index 0000000..1254033 --- /dev/null +++ b/src/test/java/edu/group5/app/model/AppStateTest.java @@ -0,0 +1,69 @@ +package edu.group5.app.model; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.user.Customer; +import edu.group5.app.model.user.User; + +public class AppStateTest { + private User nullUser; + private BigDecimal nullDonationAmount; + private Organization nullOrganization; + private String nullPaymentMethod; + + private User currentTestUser; + private BigDecimal currentTestDonationAmount; + private Organization currentTestOrganization; + private String currentTestPaymentMethod; + + @BeforeEach + void setUp() { + nullUser = null; + nullDonationAmount = null; + nullOrganization = null; + nullPaymentMethod = null; + + currentTestUser = new Customer(1, "Bob", + "Builder", "testuser@example.com", + "password123"); + + currentTestDonationAmount = BigDecimal.ZERO; + + currentTestOrganization = new Organization(1738, "TestOrg", + true, "https://testorg.example.com", true, + "A test organization", "https://testorg.example.com/logo.png"); + + currentTestPaymentMethod = "Credit Card"; + } + + @Test + void gettersAndSetters_WorkCorrectly() { + AppState appState = new AppState(); + + // Test current user + assertEquals(nullUser, appState.getCurrentUser()); + appState.setCurrentUser(currentTestUser); + assertEquals(currentTestUser, appState.getCurrentUser()); + + // Test current donation amount + assertEquals(nullDonationAmount, appState.getCurrentDonationAmount()); + appState.setCurrentDonationAmount(currentTestDonationAmount); + assertEquals(currentTestDonationAmount, appState.getCurrentDonationAmount()); + + // Test current organization + assertEquals(nullOrganization, appState.getCurrentOrganization()); + appState.setCurrentOrganization(currentTestOrganization); + assertEquals(currentTestOrganization, appState.getCurrentOrganization()); + + // Test current payment method + assertEquals(nullPaymentMethod, appState.getCurrentPaymentMethod()); + appState.setCurrentPaymentMethod(currentTestPaymentMethod); + assertEquals(currentTestPaymentMethod, appState.getCurrentPaymentMethod()); + } +} diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java index 58bd170..3b1e5ff 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -12,7 +12,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; - +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -61,6 +61,33 @@ void testConstructorThrowsIfOrganizationRepositoryIsNull() { assertEquals("OrganizationRepository can't be null", exception.getMessage()); } + @Test + void getUserDonationsReturnsEmptyMapIfNoDonations() { + assertTrue(donationService.getUserDonations(customer.getUserId()).isEmpty()); + } + + @Test + void getOrganizationDonationsReturnsMapOfDonations() { + Donation donation1 = new Donation(1, customer.getUserId(), + 101, new BigDecimal("20.00"), Timestamp.from(Instant.now()), "Card"); + Donation donation2 = new Donation(2, customer.getUserId(), + 101, new BigDecimal("30.00"), Timestamp.from(Instant.now()), "PayPal"); + donationRepository.addContent(donation1); + donationRepository.addContent(donation2); + + Map donations = donationService.getOrganizationDonations(101); + assertEquals(2, donations.size()); + assertTrue(donations.containsKey(1)); + assertTrue(donations.containsKey(2)); + assertEquals(donation1, donations.get(1)); + assertEquals(donation2, donations.get(2)); + } + + @Test + void getOrganizationDonationsReturnsEmptyMapIfNoDonations() { + assertTrue(donationService.getOrganizationDonations(1).isEmpty()); + } + @Test void donateReturnsFalseIfCustomerNull() { boolean result = donationService.donate(null, diff --git a/src/test/java/edu/group5/app/model/donation/DonationTest.java b/src/test/java/edu/group5/app/model/donation/DonationTest.java index f8f9069..1de92f9 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationTest.java @@ -74,11 +74,19 @@ void testIfThrowsExceptionWhenOrganizationIdIsNotPositive() { amount1, date1, paymentMethod1); } @Test - void testIfThrowsExceptionWhenAmountIsNotPositive() { + void testIfThrowsExceptionWhenAmountIsNegative() { expectedMessage = "Amount must be positive and not null"; exceptionTest(donationId1, userId1, organizationId1, - new BigDecimal("0.00"), date1, paymentMethod1); + new BigDecimal("-1.00"), date1, paymentMethod1); } + + @Test + void testIfThrowsExceptionWhenAmountIsNull() { + expectedMessage = "Amount must be positive and not null"; + exceptionTest(donationId1, userId1, organizationId1, + null, date1, paymentMethod1); + } + @Test void testIfThrowsExceptionWhenDateIsNull() { expectedMessage = "Date must not be null"; diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index a5eab05..e6b0cf9 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -60,6 +60,30 @@ void constructor_ThrowsWhenContentIsNull() { constructorTest(null, "Input data can't be null"); } + @Test + void constructor_SkipsOrganizationWithMissingOrgNumber() { + Object[] content = new Object[] { + Map.of( + "name", "Good Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "999", + "name", "Bad Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ) + }; + + OrganizationRepository repo = new OrganizationRepository(content, scraper); + + assertEquals(1, repo.findByOrgNumber(999) != null ? 1 : 0); + assertNull(repo.findByOrgNumber(1)); + } + @Test void constructor_ThrowsWhenScraperIsNull() { Object[] content = new Object[] { diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java index c6c318b..8807e61 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -63,4 +63,17 @@ void fetchLogoUrl_CachesResultOnSecondCall() { // If no exception thrown, cache works assertTrue(true); } + + @Test + void fetchDescription_ReturnsCachedValue() { + OrganizationScraper scraper = new OrganizationScraper(); + + // First call - caches (but makes real request) + String result1 = scraper.fetchDescription("https://example.com"); + + // Second call - should return cached without new request + String result2 = scraper.fetchDescription("https://example.com"); + + assertEquals(result1, result2); // Same object = cached + } } From 4927acac63d746e7e2bdd8177fb762f4efa83cdc Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 14:53:28 +0200 Subject: [PATCH 2/4] test[JUnit]: add Mockito dependency and more JUnit tests ensuring greater test coverage --- pom.xml | 6 ++ .../java/edu/group5/app/model/Repository.java | 8 -- .../organization/OrganizationScraper.java | 86 ++++++++++++------ .../organization/OrganizationScraperTest.java | 91 +++++++++++++++++-- .../app/model/user/UserServiceTest.java | 6 +- 5 files changed, 148 insertions(+), 49 deletions(-) diff --git a/pom.xml b/pom.xml index 5bbba22..8091ffe 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,12 @@ jsoup 1.17.2 + + org.mockito + mockito-inline + 5.2.0 + test + diff --git a/src/main/java/edu/group5/app/model/Repository.java b/src/main/java/edu/group5/app/model/Repository.java index 6dd425e..8b4ec61 100644 --- a/src/main/java/edu/group5/app/model/Repository.java +++ b/src/main/java/edu/group5/app/model/Repository.java @@ -23,12 +23,4 @@ protected Repository(Map content) { ParameterValidator.objectChecker(content, "content"); this.content = content; } - - /** - * Gets the content of the repository. - * @return the content of the repository - */ - public Map getContent() { - return content; - } } diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index ad93736..beb16aa 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -40,31 +40,10 @@ public String fetchDescription(String pageUrl) { .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") .timeout(5000).get(); - Element section = doc.selectFirst("section.information"); - if (section != null) { - section.select("div.extra-info").remove(); - section.select("a.read-more").remove(); - - // Extract all

tags and

elements as separate paragraphs - String description = section.select("p, div").stream() - .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) - .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) - .map(Element::text) - .map(text -> text.replace("Les mer", "").trim()) - .filter(text -> !text.isBlank()) - .collect(Collectors.joining("\n\n")); - - // Fallback: if no paragraphs found, get all text from section - if (description.isBlank()) { - description = section.text().trim(); - } - description = description.replace("Les mer", "").trim(); - - // Only cache and return if we found something meaningful - if (!description.isBlank()) { - descriptionCache.put(pageUrl, description); - return description; - } + String description = parseDescription(doc); + if (!description.isBlank()) { + descriptionCache.put(pageUrl, description); + return description; } } catch (Exception e) { System.out.println("Could not get description for: " + pageUrl); @@ -72,6 +51,42 @@ public String fetchDescription(String pageUrl) { return null; } + /** + * Parses the description from a Document by extracting text content + * from {@code
}. + * + * @param doc the Document to parse + * @return the description text, or empty string if not found + */ + protected String parseDescription(Document doc) { + Element section = doc.selectFirst("section.information"); + if (section != null) { + section.select("div.extra-info").remove(); + section.select("a.read-more").remove(); + + // Extract all

tags and

elements as separate paragraphs + String description = section.select("p, div").stream() + .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) + .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) + .map(Element::text) + .map(text -> text.replace("Les mer", "").trim()) + .filter(text -> !text.isBlank()) + .collect(Collectors.joining("\n\n")); + + // Fallback: if no paragraphs found, get all text from section + if (description.isBlank()) { + description = section.text().trim(); + } + description = description.replace("Les mer", "").trim(); + + // Only return if we found something meaningful + if (!description.isBlank()) { + return description; + } + } + return ""; + } + /** * Fetches the logo URL for the given page by scraping the {@code div.logo img} * element. Results are cached so each URL is only fetched once. @@ -88,10 +103,9 @@ public String fetchLogoUrl(String pageUrl) { Document doc = Jsoup.connect(pageUrl) .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") .timeout(5000).get(); - Element img = doc.selectFirst("div.logo img"); - if (img != null) { - String logoUrl = img.absUrl("src"); + String logoUrl = parseLogoUrl(doc); + if (!logoUrl.isBlank()) { logoCache.put(pageUrl, logoUrl); return logoUrl; } @@ -100,4 +114,20 @@ public String fetchLogoUrl(String pageUrl) { } return null; } + + /** + * Parses the logo URL from a Document by extracting the image src + * from {@code div.logo img}. + * + * @param doc the Document to parse + * @return the absolute logo URL, or empty string if not found + */ + protected String parseLogoUrl(Document doc) { + Element img = doc.selectFirst("div.logo img"); + if (img != null) { + String logoUrl = img.absUrl("src"); + return logoUrl.isBlank() ? "" : logoUrl; + } + return ""; + } } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java index 8807e61..e78c492 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -2,8 +2,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.mockito.MockedStatic; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; class OrganizationScraperTest { @@ -15,23 +22,23 @@ void setUp() { } @Test - void fetchDescription_ReturnsNullWhenUrlIsNull() { + void fetchDescriptionReturnsNullWhenUrlIsNull() { assertNull(scraper.fetchDescription(null)); } @Test - void fetchDescription_ReturnsNullWhenUrlIsBlank() { + void fetchDescriptionReturnsNullWhenUrlIsBlank() { assertNull(scraper.fetchDescription("")); } @Test - void fetchDescription_ReturnsNullWhenUrlIsInvalid() { + void fetchDescriptionReturnsNullWhenUrlIsInvalid() { String result = scraper.fetchDescription("https://invalid-url-that-does-not-exist-xyz123.com"); assertNull(result); } @Test - void fetchDescription_CachesResultOnSecondCall() { + void fetchDescriptionCachesResultOnSecondCall() { // Mock URLs won't work, but cache still works with null returns scraper.fetchDescription("https://example.com"); scraper.fetchDescription("https://example.com"); @@ -40,23 +47,23 @@ void fetchDescription_CachesResultOnSecondCall() { } @Test - void fetchLogoUrl_ReturnsNullWhenUrlIsNull() { + void fetchLogoUrlReturnsNullWhenUrlIsNull() { assertNull(scraper.fetchLogoUrl(null)); } @Test - void fetchLogoUrl_ReturnsNullWhenUrlIsBlank() { + void fetchLogoUrlReturnsNullWhenUrlIsBlank() { assertNull(scraper.fetchLogoUrl("")); } @Test - void fetchLogoUrl_ReturnsNullWhenUrlIsInvalid() { + void fetchLogoUrlReturnsNullWhenUrlIsInvalid() { String result = scraper.fetchLogoUrl("https://invalid-url-that-does-not-exist-xyz123.com"); assertNull(result); } @Test - void fetchLogoUrl_CachesResultOnSecondCall() { + void fetchLogoUrlCachesResultOnSecondCall() { // Mock URLs won't work, but cache still works with null returns scraper.fetchLogoUrl("https://example.com"); scraper.fetchLogoUrl("https://example.com"); @@ -65,7 +72,7 @@ void fetchLogoUrl_CachesResultOnSecondCall() { } @Test - void fetchDescription_ReturnsCachedValue() { + void fetchDescriptionReturnsCachedValue() { OrganizationScraper scraper = new OrganizationScraper(); // First call - caches (but makes real request) @@ -76,4 +83,68 @@ void fetchDescription_ReturnsCachedValue() { assertEquals(result1, result2); // Same object = cached } -} + + @Test + void fetchDescriptionHandlesExceptionGracefully() { + String result = scraper.fetchDescription("https://invalid-domain-xyz.test"); + assertNull(result); + } + + @Test + void fetchLogoUrlHandlesExceptionGracefully() { + String result = scraper.fetchLogoUrl("https://invalid-domain-xyz.test"); + assertNull(result); + } + + @Test + void parseDescriptionExtractsParagraphs() { + String html = "

First paragraph

Second paragraph

"; + Document doc = Jsoup.parse(html); + assertTrue(scraper.parseDescription(doc).contains("First paragraph")); + } + + @Test + void parseDescriptionUsesFallbackWhenNoParagraphs() { + String html = "
Fallback text without paragraph tags
"; + Document doc = Jsoup.parse(html); + assertEquals("Fallback text without paragraph tags", scraper.parseDescription(doc)); + } + + @Test + void parseDescriptionRemovesExtraInfoDivs() { + String html = "

Keep this

Remove this
"; + Document doc = Jsoup.parse(html); + String result = scraper.parseDescription(doc); + assertTrue(result.contains("Keep this")); + assertFalse(result.contains("Remove this")); + } + + @Test + void parseDescriptionFiltersOutLesMer() { + String html = "

Some text Les mer More text

"; + Document doc = Jsoup.parse(html); + assertFalse(scraper.parseDescription(doc).contains("Les mer")); + } + + @Test + void parseDescriptionReturnsEmptyStringWhenNoSection() { + String html = "
No section here
"; + Document doc = Jsoup.parse(html); + assertEquals("", scraper.parseDescription(doc)); + } + + @Test + void parseLogoUrlExtractsImageUrl() { + String html = "
"; + Document doc = Jsoup.parse(html); + doc.setBaseUri("https://example.com"); + assertTrue(scraper.parseLogoUrl(doc).contains("logo.png")); + } + + @Test + void parseLogoUrlReturnsEmptyWhenNoImage() { + String html = "
"; + Document doc = Jsoup.parse(html); + assertEquals("", scraper.parseLogoUrl(doc)); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java index 104e2f1..459b635 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -71,11 +71,11 @@ void registerUserInvalidInputsReturnFalse() { } @Test - void registerUserDuplicateEmailAllowedInCurrentCode() { + void registerUserDuplicateEmailNotAllowedInCurrentCode() { boolean result = service.registerUser("Customer", "John", "Cena", "john.cena@example.com", "$2a$10$hashed"); - assertTrue(result); - assertEquals(3, repo.getUsers().size()); + assertFalse(result); + assertEquals(2, repo.getUsers().size()); } @Test From fe3e366c72f6339b9ccc3af101dc421881cea886 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 14:59:50 +0200 Subject: [PATCH 3/4] fix[OrganizationPageView]: fix picture quality on Organization logo --- .../group5/app/view/organizationpage/OrganizationPageView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index f591d26..7ea3b6b 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -96,7 +96,7 @@ private StackPane createImageContainer() { // Load image in background thread to avoid blocking UI new Thread(() -> { try { - Image image = new Image(org.logoUrl(), 120, 120, true, true); + Image image = new Image(org.logoUrl(), 350, 350, true, true); Platform.runLater(() -> { ImageView logo = new ImageView(image); logo.setId("logo"); From b317fbd1cc0258ded58108a4400d0e93b4ef8649 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 15:07:09 +0200 Subject: [PATCH 4/4] fix[pom.xml]: remove Mockito dependency due to it being unused in the JUnit tests --- pom.xml | 6 ------ .../app/model/organization/OrganizationScraperTest.java | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 8091ffe..5bbba22 100644 --- a/pom.xml +++ b/pom.xml @@ -73,12 +73,6 @@ jsoup 1.17.2 - - org.mockito - mockito-inline - 5.2.0 - test - diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java index e78c492..5d48428 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -4,13 +4,9 @@ import org.junit.jupiter.api.Test; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; -import org.mockito.MockedStatic; + import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; class OrganizationScraperTest {