From adaec06083ea7ed687f2860354fedadb94ec00b6 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 10:22:07 +0200 Subject: [PATCH 01/31] Fix: Validator for stock symbol rename --- .../edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java index 6d12cbc..b31108e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java @@ -55,8 +55,8 @@ public enum Validator { /** * Rule that checks if a string is considered a valid stock name. * */ - VALID_STOCK_NAME(NOT_EMPTY.validationRule.and(s -> - s.length() == 4), "Invalid stock name!"), + VALID_STOCK_SYMBOL(NOT_EMPTY.validationRule.and(s -> + s.length() == 4), "Invalid stock symbol!"), /** * Rule that checks if a string represents a positive integer. From 19611bd6935ca2c58e0f442f909ebcdb7e7bab02 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 11:38:02 +0200 Subject: [PATCH 02/31] Feat: Basic improvements, testing. --- .../idatt2003/g40/mappe/engine/Exchange.java | 119 ++++++++----- .../idatt2003/g40/mappe/model/Portfolio.java | 4 +- .../idi/idatt2003/g40/mappe/model/Stock.java | 4 +- .../view/widgets/market/MarketController.java | 2 +- .../g40/mappe/engine/ExchangeTest.java | 165 ++++++++++++++---- 5 files changed, 214 insertions(+), 80 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index b73f634..a3a9af3 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -1,31 +1,25 @@ package edu.ntnu.idi.idatt2003.g40.mappe.engine; import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; -import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; -import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; import edu.ntnu.idi.idatt2003.g40.mappe.service.*; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventPublisher; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; /** * Represents a stock exchange where stocks can be traded. * - *

Holds a map of stocks where stock symbol is key and stock is value.

+ *

Holds a map of {@link Stock} objects where stock symbol is key and + * stock is value.

* *

Delegates buying and selling to player elements using calculators

* @@ -33,6 +27,8 @@ * * @see Player * @see TransactionCalculator + * + * @version 1.0.0 * */ public final class Exchange { @@ -59,13 +55,16 @@ public final class Exchange { /** * Constructor. * - * @param name name of exchange. - * @param stocks list of {@link Stock} objects. + * @param name name of exchange. + * @param stocks list of {@link Stock} objects. * * @throws IllegalArgumentException if name or stocks are empty/null. * */ - public Exchange(final String name, final List stocks) throws IllegalArgumentException { - if (!Validator.NOT_EMPTY.isValid(name) || stocks == null || stocks.isEmpty()) { + public Exchange(final String name, final List stocks) + throws IllegalArgumentException { + if (!Validator.NOT_EMPTY.isValid(name) + || stocks == null + || stocks.isEmpty()) { throw new IllegalArgumentException("Invalid exchange parameters!"); } this.name = name; @@ -108,7 +107,7 @@ public IntegerProperty weekProperty() { * * @param symbol the stock symbol. * - * @return true or false. + * @return true or false. * */ public boolean hasStock(final String symbol) { return stockMap.containsKey(symbol); @@ -117,16 +116,16 @@ public boolean hasStock(final String symbol) { /** * Getter method for stock element. * - * @param symbol the symbol of the stock to get. + * @param symbol the symbol of the stock to get. * - * @return {@link Stock} element gotten. + * @return {@link Stock} element gotten. * * @throws IllegalArgumentException if symbol is invalid. * */ public Stock getStock(final String symbol) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( - Validator.VALID_STOCK_NAME.getErrorMessage()); + Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } return stockMap.get(symbol); } @@ -136,7 +135,7 @@ public Stock getStock(final String symbol) throws IllegalArgumentException { * * @param searchTerm the term to search for. * - * @return a list of {@link Stock} objects. + * @return a list of {@link Stock} objects. * */ public List findStocks(final String searchTerm) { List result = new ArrayList<>(); @@ -154,24 +153,26 @@ public List findStocks(final String searchTerm) { /** * Method called when a player buys a stock. * - * @param symbol the stock this player buys. - * @param quantity the amount of stock to buy. - * @param player the player buying stock. + * @param symbol the stock this player buys. + * @param quantity the amount of stock to buy. + * @param player the player buying stock. * - * @return Transaction representing the transaction. + * @return Transaction representing the transaction. * * @throws IllegalArgumentException if symbol or player is invalid. * */ public Transaction buy(final String symbol, final BigDecimal quantity, final Player player) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol) || player == null) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) || player == null) { throw new IllegalArgumentException("Invalid purchase!"); } Stock stock = stockMap.get(symbol); Share share = new Share(stock, quantity, stock.getSalesPrice()); TransactionCalculator calculator = new PurchaseCalculator(share); - Purchase purchase = new Purchase(share, week.get(), calculator); + Transaction purchase = TransactionFactory.createTransaction( + TransactionType.PURCHASE, share, getWeek(), calculator + ); player.handleTransaction(purchase); return purchase; } @@ -179,10 +180,10 @@ public Transaction buy(final String symbol, /** * Method called when a player sells share. * - * @param share the share to sell. - * @param player the player buying stock. + * @param share the share to sell. + * @param player the player buying stock. * - * @return Transaction representing the transaction. + * @return Transaction representing the transaction. * * @throws IllegalArgumentException if share or player is null. * */ @@ -192,27 +193,37 @@ public Transaction sell(final Share share, final Player player) throw new IllegalArgumentException("Invalid sell!"); } TransactionCalculator calculator = new SaleCalculator(share); - Sale sale = new Sale(share, week.get(), calculator); + Transaction sale = TransactionFactory.createTransaction( + TransactionType.SALE, share, getWeek(), calculator + ); player.handleTransaction(sale); return sale; } /** - * Method called when a player sells share. + * Method called when a player sells share, + * defined by an amount instead of specific {@link Share} object. + * + *

Might split shares into multiples to ensure proper selling. + * Uses the {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio} + * to split.

* - * @param amount the amount of "shares" to sell. + * @param amount the amount of "shares" to sell. * @param stockSymbol the stock to sell shares in. - * @param player the player buying stock. + * @param player the player buying stock. * - * @return Transaction representing the transaction. + * @return List of transactions commited to sell the shares. * - * @throws IllegalArgumentException if any parameter is null, or if player does not have enough shares. + * @throws IllegalArgumentException if any parameter is null, + * or if player does not have enough shares. * */ public List sell(BigDecimal amount, final String stockSymbol, final Player player) throws IllegalArgumentException { - if (amount == null || player == null || !Validator.NOT_EMPTY.isValid(stockSymbol)) { + if (amount == null + || player == null + || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) { throw new IllegalArgumentException("Invalid sell!"); } else { @@ -220,7 +231,8 @@ public List sell(BigDecimal amount, .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) .toList(); - BigDecimal totalOwned = player.getPortfolio().getTotalSharesBySymbol(stockSymbol); + BigDecimal totalOwned = player.getPortfolio() + .getTotalSharesBySymbol(stockSymbol); if (amount.compareTo(totalOwned) > 0) { amount = totalOwned; @@ -239,7 +251,9 @@ public List sell(BigDecimal amount, remainingToSell = remainingToSell.subtract(shareQty); transactions.add(sell(share, player)); } else { - Share newShare = player.getPortfolio().splitShare(share, remainingToSell); + Share newShare = player.getPortfolio().splitShare( + share, remainingToSell + ); remainingToSell = BigDecimal.ZERO; transactions.add(sell(newShare, player)); } @@ -250,12 +264,19 @@ public List sell(BigDecimal amount, /** * Method for advancing time, increasing the amount of weeks. + * + *

Applies a random price change from -5% to 5% to every stock, + * plus a flat percent determined by their fortune, that can range from + * -10% to +10%.

+ * + * @see edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.GameEngineView * */ public void advance() { for (Stock stock : stockMap.values()) { BigDecimal currentPrice = stock.getSalesPrice(); - double change = ((random.nextDouble() * 0.10) - 0.05) + stock.getFortune(); + double change = ( + (random.nextDouble() * 0.10) - 0.05) + stock.getFortune(); stock.setFortune(0); BigDecimal factor = BigDecimal.valueOf(1 + change); @@ -269,11 +290,17 @@ public void advance() { * Method for getting the stocks with the most * amount of increase since last week. * - * @param limit the maximum amount of stocks returned + * @param limit the maximum amount of stocks returned * * @return list of {@link Stock} objects. + * + * @throws IllegalArgumentException if limit is invalid (negative or zero). * */ - public List getGainers(final int limit) { + public List getGainers(final int limit) + throws IllegalArgumentException { + if (limit < 1) { + throw new IllegalArgumentException("Invalid limit for getting gainers!"); + } return stockMap.entrySet().stream() // We only want the stocks with a positive price change. .filter(e -> @@ -294,11 +321,17 @@ public List getGainers(final int limit) { * Method for getting the stocks with the highest * loss of price since last week. * - * @param limit the maximum amount of stocks returned + * @param limit the maximum amount of stocks returned * * @return list of {@link Stock} objects. + * + * @throws IllegalArgumentException if limit is invalid (negative or zero). * */ - public List getLosers(final int limit) { + public List getLosers(final int limit) + throws IllegalArgumentException { + if (limit < 1) { + throw new IllegalArgumentException("Invalid limit for getting losers!"); + } return stockMap.entrySet().stream() // Only get entries with negative price change. .filter(e -> diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index 483adb8..0c7ea5e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -82,9 +82,9 @@ public List getShares() { * @throws IllegalArgumentException if symbol is invalid. */ public List getShares(final String symbol) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( - Validator.VALID_STOCK_NAME.getErrorMessage()); + Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } return shares.stream() .filter(s -> symbol.equalsIgnoreCase(s.getStock().getSymbol())) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java index 1ca9664..0aff105 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java @@ -44,9 +44,9 @@ public final class Stock { public Stock(final String symbol, final String company, final BigDecimal salesPrice) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( - Validator.VALID_STOCK_NAME.getErrorMessage()); + Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } else { this.symbol = symbol; this.company = company; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java index e185fb3..86838a9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java @@ -84,7 +84,7 @@ protected void initInteractions() { * @return the total quantity, or 0 if the symbol is invalid. * */ private int ownedQuantity(final String symbol) { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { return 0; } return player.getPortfolio().getShares(symbol).stream() diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java index 8f4f1a5..e4dd5ee 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java @@ -4,98 +4,183 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; import java.math.BigDecimal; import java.util.List; -import edu.ntnu.idi.idatt2003.g40.mappe.model.*; +import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class ExchangeTest { + /** + * Test sample stock. + * */ private Stock appleStock; + /** + * Test sample stock. + * */ + private Stock teslaStock; + + /** + * Test sample stock. + * */ + private Stock pearStock; + + /** + * Test exchange to use. + * */ + private Exchange testExchange; + + /** + * Test player for testing transactions. + * */ + private Player testPlayer; + @BeforeEach void setUp() { appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); + teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200")); + pearStock = new Stock("Pear", "Pear Inc.", new BigDecimal("97")); + testExchange = new Exchange("NASDAQ", List.of(appleStock, teslaStock)); + testPlayer = new Player("Alice", new BigDecimal("1000")); } @Test void constructorSetsNameWeekAndStocksCorrectly() { - Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200")); - - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock, tesla)); - - assertEquals("NASDAQ", exchange.getName()); - assertEquals(1, exchange.getWeek()); - assertTrue(exchange.hasStock("AAPL")); - assertTrue(exchange.hasStock("TSLA")); + assertEquals("NASDAQ", testExchange.getName()); + assertEquals(1, testExchange.getWeek()); + assertTrue(testExchange.hasStock("AAPL")); + assertTrue(testExchange.hasStock("TSLA")); } @Test void getStockReturnsCorrectStock() { - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock)); - - Stock result = exchange.getStock("AAPL"); + Stock result = testExchange.getStock("AAPL"); assertSame(appleStock, result); } @Test void findStocksReturnsMatchingStocksBySymbolOrCompany() { - Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200")); - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock, tesla)); - - List resultBySymbol = exchange.findStocks("AAP"); - List resultByCompany = exchange.findStocks("tes"); + List resultBySymbol = testExchange.findStocks("AAP"); + List resultByCompany = testExchange.findStocks("tes"); assertEquals(1, resultBySymbol.size()); assertTrue(resultBySymbol.contains(appleStock)); assertEquals(1, resultByCompany.size()); - assertTrue(resultByCompany.contains(tesla)); + assertTrue(resultByCompany.contains(teslaStock)); } @Test void buyReturnsPurchaseAndWithdrawsMoneyFromPlayer() { - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock)); - Player player = new Player("Alice", new BigDecimal("1000")); + Transaction transaction = + testExchange.buy("AAPL", new BigDecimal("2"), testPlayer); + + TransactionCalculator calculator = + new PurchaseCalculator(transaction.getShare()); - Transaction transaction = exchange.buy("AAPL", new BigDecimal("2"), player); + BigDecimal expectedPlayerMoney = + testPlayer.getStartingMoney().subtract(calculator.calculateTotal()); assertInstanceOf(Purchase.class, transaction); assertEquals(1, transaction.getWeek()); assertEquals(new BigDecimal("2"), transaction.getShare().getQuantity()); - assertEquals(new BigDecimal("100"), transaction.getShare() + assertEquals(appleStock.getSalesPrice(), transaction.getShare() .getPurchasePrice()); - assertEquals(new BigDecimal("799.000"), player.getMoney()); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); } @Test void sellReturnsSaleAndAddsMoneyToPlayer() { - Stock apple = new Stock("AAPL", "Apple", new BigDecimal("150")); - Exchange exchange = new Exchange("NASDAQ", List.of(apple)); - Player player = new Player("Alice", new BigDecimal("1000")); - Share share = new Share(apple, new BigDecimal("2"), new BigDecimal("100")); + Share share = + new Share(appleStock, new BigDecimal("2"), appleStock.getSalesPrice()); - Transaction transaction = exchange.sell(share, player); + Transaction transaction = testExchange.sell(share, testPlayer); + + TransactionCalculator calculator = new SaleCalculator(share); + BigDecimal expectedPlayerMoney = + testPlayer.getStartingMoney().add(calculator.calculateTotal()); assertInstanceOf(Sale.class, transaction); assertEquals(1, transaction.getWeek()); assertSame(share, transaction.getShare()); - assertEquals(new BigDecimal("1267.9000"), player.getMoney()); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + } + + @Test + void sellingAmountOfSharesRequiringSplitFunctions() { + Transaction purchase = testExchange.buy( + appleStock.getSymbol(), new BigDecimal("3"), testPlayer + ); + + List sales = testExchange.sell( + new BigDecimal("1.5"), + appleStock.getSymbol(), + testPlayer + ); + + SaleCalculator saleCalculator = new SaleCalculator(sales.getFirst().getShare()); + PurchaseCalculator purchaseCalculator = new PurchaseCalculator(purchase.getShare()); + BigDecimal expectedPlayerMoney = testPlayer.getStartingMoney() + .subtract(purchaseCalculator.calculateTotal()) + .add(saleCalculator.calculateTotal()); + + assertEquals(1, sales.size()); + + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + + BigDecimal expectedShareAmountOwned = purchase.getShare().getQuantity() + .subtract(sales.getFirst().getShare().getQuantity()); + + assertEquals(expectedShareAmountOwned, + testPlayer + .getPortfolio() + .getShares(appleStock.getSymbol()) + .getFirst() + .getQuantity() + ); + } + + @Test + void sellingAmountOfSharesRequiringSplitFunctions2() { + // Arrange: Clear setup with explicit numbers + Transaction purchase1 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("3.12"), testPlayer); + Transaction purchase2 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("4.56"), testPlayer); + + // Act: Sell 6 shares (requires taking 3.12 from purchase1 and 2.88 from purchase2) + List sales = testExchange.sell(new BigDecimal("6.00"), appleStock.getSymbol(), testPlayer); + + // Assert 1: Verify the split logic actually happened + assertEquals(2, sales.size(), "Should split the sale across two purchase records"); + assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); + assertEquals(new BigDecimal("2.88"), sales.get(1).getShare().getQuantity()); + + // Assert 2: Verify player money matches a known, pre-calculated constant + BigDecimal expectedFinalMoney = new BigDecimal("822.16000"); // Replace with the actual hardcoded math result + assertEquals(expectedFinalMoney, testPlayer.getMoney()); } @Test void advanceIncreasesWeekAndUpdatesStockPrice() { - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock)); BigDecimal oldPrice = appleStock.getSalesPrice(); - exchange.advance(); + testExchange.advance(); - assertEquals(2, exchange.getWeek()); + assertEquals(2, testExchange.getWeek()); assertNotEquals(oldPrice, appleStock.getSalesPrice()); } @@ -146,4 +231,20 @@ void getLosersActuallyReturnsProperLosers() { boolean actualLosersNotContainingLoserOutsideOfLimit = !actualLosers.contains(pearStock); assertTrue(actualLosersNotContainingLoserOutsideOfLimit); } + + @Test + void invalidLimitForGettingGainersThrowError() { + Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + + assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(0)); + assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-1)); + } + + @Test + void invalidLimitForGettingLosersThrowError() { + Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + + assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(0)); + assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1)); + } } From 3ee92965fe364d8792330024dd458f328e95a8fe Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 15:13:21 +0200 Subject: [PATCH 03/31] Feat: Refactored Exchange, testing Refactored Exchange, fixed semantic bugs, centralized logic, and made integerproperty read only for safety and encapsulation --- .../idatt2003/g40/mappe/engine/Exchange.java | 102 +++++---- .../g40/mappe/engine/ExchangeTest.java | 215 +++++++++++++----- 2 files changed, 221 insertions(+), 96 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index a3a9af3..bc1ec00 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -4,7 +4,11 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import edu.ntnu.idi.idatt2003.g40.mappe.service.*; +import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.math.BigDecimal; import java.util.ArrayList; @@ -13,7 +17,7 @@ import java.util.Map; import java.util.Random; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; /** * Represents a stock exchange where stocks can be traded. @@ -40,7 +44,7 @@ public final class Exchange { /** * Current week (set to 1 in constructor). * */ - private final IntegerProperty week = new SimpleIntegerProperty(1); + private final ReadOnlyIntegerWrapper week = new ReadOnlyIntegerWrapper(1); /** * Map of {@link Stock} objects. Key is stock symbol. Value is stock. @@ -120,10 +124,12 @@ public boolean hasStock(final String symbol) { * * @return {@link Stock} element gotten. * - * @throws IllegalArgumentException if symbol is invalid. + * @throws IllegalArgumentException if symbol is invalid or not in exchange. * */ - public Stock getStock(final String symbol) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { + public Stock getStock(final String symbol) + throws IllegalArgumentException { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) + || !stockMap.containsKey(symbol)) { throw new IllegalArgumentException( Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } @@ -164,10 +170,12 @@ public List findStocks(final String searchTerm) { public Transaction buy(final String symbol, final BigDecimal quantity, final Player player) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) || player == null) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) + || quantity == null + || player == null) { throw new IllegalArgumentException("Invalid purchase!"); } - Stock stock = stockMap.get(symbol); + Stock stock = getStock(symbol); Share share = new Share(stock, quantity, stock.getSalesPrice()); TransactionCalculator calculator = new PurchaseCalculator(share); Transaction purchase = TransactionFactory.createTransaction( @@ -212,10 +220,12 @@ TransactionType.SALE, share, getWeek(), calculator * @param stockSymbol the stock to sell shares in. * @param player the player buying stock. * - * @return List of transactions commited to sell the shares. + * @return List of transactions commited to sell the shares. * * @throws IllegalArgumentException if any parameter is null, - * or if player does not have enough shares. + * or if player does not have enough shares, + * or if player does not own any shares + * of the given stock. * */ public List sell(BigDecimal amount, final String stockSymbol, @@ -225,41 +235,50 @@ public List sell(BigDecimal amount, || player == null || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) { throw new IllegalArgumentException("Invalid sell!"); - } else { + } - List sharesOfStock = player.getPortfolio().getShares().stream() - .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) - .toList(); + // Get all shares the player owns of the stock given in the parameter. + List sharesOfStock = player.getPortfolio().getShares().stream() + .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) + .toList(); - BigDecimal totalOwned = player.getPortfolio() - .getTotalSharesBySymbol(stockSymbol); + // Throws error if player does not own any shares of the given stock. + if (sharesOfStock.isEmpty()) { + throw new IllegalArgumentException("Player does not own" + + " any shares of this stock!"); + } - if (amount.compareTo(totalOwned) > 0) { - amount = totalOwned; - } - ArrayList transactions = new ArrayList<>(); - BigDecimal remainingToSell = amount; + // Gets the total quantity amount of shares owned of given stock. + BigDecimal totalOwned = player.getPortfolio() + .getTotalSharesBySymbol(stockSymbol); + + // If amount wanted to sell is greater than total owned, + // sells entire collection. + if (amount.compareTo(totalOwned) > 0) { + amount = totalOwned; + } + ArrayList transactions = new ArrayList<>(); + BigDecimal remainingToSell = amount; - for (Share share : sharesOfStock) { - if (remainingToSell.compareTo(BigDecimal.ZERO) <= 0) { - break; - } + // For every share owned, sells if the quantity is less than + // or equal to the remaining amount of shares to sell. + // Splits share if the remaining amount to sell is less than share quantity. + for (Share share : sharesOfStock) { - BigDecimal shareQty = share.getQuantity(); + BigDecimal shareQty = share.getQuantity(); - if (shareQty.compareTo(remainingToSell) <= 0) { - remainingToSell = remainingToSell.subtract(shareQty); - transactions.add(sell(share, player)); - } else { - Share newShare = player.getPortfolio().splitShare( - share, remainingToSell - ); - remainingToSell = BigDecimal.ZERO; - transactions.add(sell(newShare, player)); - } + if (shareQty.compareTo(remainingToSell) <= 0) { + remainingToSell = remainingToSell.subtract(shareQty); + transactions.add(sell(share, player)); + } else { + Share newShare = player.getPortfolio().splitShare( + share, remainingToSell + ); + transactions.add(sell(newShare, player)); + break; } - return transactions; } + return transactions; } /** @@ -275,12 +294,13 @@ public void advance() { for (Stock stock : stockMap.values()) { BigDecimal currentPrice = stock.getSalesPrice(); - double change = ( - (random.nextDouble() * 0.10) - 0.05) + stock.getFortune(); + BigDecimal change = BigDecimal.valueOf(random.nextDouble() * 0.10 - 0.05) + .add(BigDecimal.valueOf(stock.getFortune())); stock.setFortune(0); - BigDecimal factor = BigDecimal.valueOf(1 + change); + BigDecimal factor = BigDecimal.ONE.add(change); - BigDecimal newPrice = currentPrice.multiply(factor); + BigDecimal newPrice = currentPrice.multiply(factor) + .setScale(2, java.math.RoundingMode.HALF_UP); stock.addNewSalesPrice(newPrice); } week.set(week.get() + 1); diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java index e4dd5ee..147810b 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java @@ -1,6 +1,8 @@ package edu.ntnu.idi.idatt2003.g40.mappe.engine; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; @@ -13,12 +15,12 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import java.math.BigDecimal; -import java.util.List; - import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,7 +56,8 @@ void setUp() { appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200")); pearStock = new Stock("Pear", "Pear Inc.", new BigDecimal("97")); - testExchange = new Exchange("NASDAQ", List.of(appleStock, teslaStock)); + testExchange = new Exchange("NASDAQ", + List.of(appleStock, teslaStock, pearStock)); testPlayer = new Player("Alice", new BigDecimal("1000")); } @@ -62,10 +65,27 @@ void setUp() { void constructorSetsNameWeekAndStocksCorrectly() { assertEquals("NASDAQ", testExchange.getName()); assertEquals(1, testExchange.getWeek()); + assertEquals(1, testExchange.weekProperty().intValue()); assertTrue(testExchange.hasStock("AAPL")); assertTrue(testExchange.hasStock("TSLA")); } + @Test + void constructorThrowsErrorOnInvalidParameters() { + assertDoesNotThrow(() -> + new Exchange("Valid name", + List.of(appleStock, teslaStock, pearStock))); + + assertThrows(IllegalArgumentException.class, () -> + new Exchange(null, List.of(appleStock, teslaStock, pearStock))); + + assertThrows(IllegalArgumentException.class, () -> + new Exchange("Valid name", null)); + + assertThrows(IllegalArgumentException.class, () -> + new Exchange("Valid name", new ArrayList<>())); + } + @Test void getStockReturnsCorrectStock() { Stock result = testExchange.getStock("AAPL"); @@ -73,6 +93,17 @@ void getStockReturnsCorrectStock() { assertSame(appleStock, result); } + @Test + void getStockThrowsExceptionOnInvalidStock() { + assertDoesNotThrow( + () -> testExchange.getStock("AAPL") + ); + assertThrows(IllegalArgumentException.class, + () -> testExchange.getStock("INVALIDSYMBOL")); + assertThrows(IllegalArgumentException.class, + () -> testExchange.getStock("NVID")); + } + @Test void findStocksReturnsMatchingStocksBySymbolOrCompany() { List resultBySymbol = testExchange.findStocks("AAP"); @@ -104,10 +135,30 @@ void buyReturnsPurchaseAndWithdrawsMoneyFromPlayer() { assertEquals(expectedPlayerMoney, testPlayer.getMoney()); } + @Test + void buyThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow(() -> + testExchange.buy("AAPL", new BigDecimal("2"), testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy("NVID", new BigDecimal("2"), testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy(null, new BigDecimal("2"), testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy("AAPL", null, testPlayer)); + + assertThrows(IllegalArgumentException.class, () -> + testExchange.buy("AAPL", new BigDecimal("2"), null)); + + } + @Test void sellReturnsSaleAndAddsMoneyToPlayer() { Share share = - new Share(appleStock, new BigDecimal("2"), appleStock.getSalesPrice()); + new Share(appleStock, new BigDecimal("2"), + appleStock.getSalesPrice()); Transaction transaction = testExchange.sell(share, testPlayer); @@ -122,31 +173,33 @@ void sellReturnsSaleAndAddsMoneyToPlayer() { } @Test - void sellingAmountOfSharesRequiringSplitFunctions() { - Transaction purchase = testExchange.buy( - appleStock.getSymbol(), new BigDecimal("3"), testPlayer - ); + void sellShareObjectThrowsExceptionOnIllegalArguments() { + Transaction transaction = testExchange.buy("AAPL", new BigDecimal("2"), testPlayer); + Share shareObject = transaction.getShare(); + assertDoesNotThrow(() -> + testExchange.sell(shareObject, testPlayer)); - List sales = testExchange.sell( - new BigDecimal("1.5"), - appleStock.getSymbol(), - testPlayer - ); + assertThrows(IllegalArgumentException.class, () -> + testExchange.sell(null, testPlayer)); - SaleCalculator saleCalculator = new SaleCalculator(sales.getFirst().getShare()); - PurchaseCalculator purchaseCalculator = new PurchaseCalculator(purchase.getShare()); - BigDecimal expectedPlayerMoney = testPlayer.getStartingMoney() - .subtract(purchaseCalculator.calculateTotal()) - .add(saleCalculator.calculateTotal()); + assertThrows(IllegalArgumentException.class, () -> + testExchange.sell(shareObject, null)); + } + + @Test + void sellingPartialSharesUpdatesPlayerMoneyAndPortfolioCorrectly() { + testExchange.buy(appleStock.getSymbol(), new BigDecimal("3"), testPlayer); + + List sales = + testExchange.sell(new BigDecimal("1.5"), appleStock.getSymbol(), testPlayer); assertEquals(1, sales.size()); + BigDecimal expectedPlayerMoney = new BigDecimal("847.000"); assertEquals(expectedPlayerMoney, testPlayer.getMoney()); - BigDecimal expectedShareAmountOwned = purchase.getShare().getQuantity() - .subtract(sales.getFirst().getShare().getQuantity()); - - assertEquals(expectedShareAmountOwned, + BigDecimal expectedRemainingShares = new BigDecimal("1.5"); + assertEquals(expectedRemainingShares, testPlayer .getPortfolio() .getShares(appleStock.getSymbol()) @@ -156,22 +209,64 @@ void sellingAmountOfSharesRequiringSplitFunctions() { } @Test - void sellingAmountOfSharesRequiringSplitFunctions2() { - // Arrange: Clear setup with explicit numbers - Transaction purchase1 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("3.12"), testPlayer); - Transaction purchase2 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("4.56"), testPlayer); + void sellingPartialAmountOfSharesRequiringSplitSplitsSharesCorrectly() { + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("3.12"), testPlayer); + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("4.56"), testPlayer); - // Act: Sell 6 shares (requires taking 3.12 from purchase1 and 2.88 from purchase2) - List sales = testExchange.sell(new BigDecimal("6.00"), appleStock.getSymbol(), testPlayer); + List sales = + testExchange.sell(new BigDecimal("6.00"), + appleStock.getSymbol(), testPlayer); - // Assert 1: Verify the split logic actually happened - assertEquals(2, sales.size(), "Should split the sale across two purchase records"); + assertEquals(2, sales.size()); assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); assertEquals(new BigDecimal("2.88"), sales.get(1).getShare().getQuantity()); - // Assert 2: Verify player money matches a known, pre-calculated constant - BigDecimal expectedFinalMoney = new BigDecimal("822.16000"); // Replace with the actual hardcoded math result - assertEquals(expectedFinalMoney, testPlayer.getMoney()); + BigDecimal expectedPlayerMoney = new BigDecimal("822.16000"); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + } + + @Test + void attemptingToSellMoreSharesThanOwnedSellsAllSharesOwned() { + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("3.12"), testPlayer); + testExchange.buy(appleStock.getSymbol(), + new BigDecimal("4.56"), testPlayer); + + List sales = + testExchange.sell(new BigDecimal("100.00"), + appleStock.getSymbol(), testPlayer); + assertEquals(2, sales.size()); + + assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); + assertEquals(new BigDecimal("4.56"), sales.get(1).getShare().getQuantity()); + + BigDecimal expectedPlayerMoney = new BigDecimal("988.48000"); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + } + + @Test + void attemptingToSellPartialAmountOfSharesWithStockNotOwnedByPlayerThrowsError() { + BigDecimal testAmount = new BigDecimal("6.00"); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(testAmount, + appleStock.getSymbol(), testPlayer)); + } + + @Test + void attemptingToSellPartialAmountOfSharesWithInvalidNumberAndPlayerThrowsError() { + BigDecimal testAmount = new BigDecimal("6.00"); + testExchange.buy(appleStock.getSymbol(), testAmount, testPlayer); + + assertDoesNotThrow(() -> testExchange.sell(testAmount, + appleStock.getSymbol(), testPlayer)); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(null, appleStock.getSymbol(), testPlayer)); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(testAmount, null, testPlayer)); + assertThrows(IllegalArgumentException.class, + () -> testExchange.sell(testAmount, appleStock.getSymbol(), null)); } @Test @@ -186,16 +281,11 @@ void advanceIncreasesWeekAndUpdatesStockPrice() { @Test void getGainersActuallyReturnsProperGainers() { - Stock teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200.00")); - Stock pearStock = new Stock("PEAR", "Pear inc.", new BigDecimal("97.00")); - appleStock.addNewSalesPrice(new BigDecimal("150.00")); teslaStock.addNewSalesPrice(new BigDecimal("230.00")); pearStock.addNewSalesPrice(new BigDecimal("112.00")); - Exchange exchange = new Exchange("Exchange", List.of(appleStock, teslaStock, pearStock)); - - List actualGainers = exchange.getGainers(2); + List actualGainers = testExchange.getGainers(2); boolean actualGainersContainLimitedGainers = actualGainers.contains(teslaStock) && actualGainers.contains(appleStock); @@ -209,17 +299,23 @@ void getGainersActuallyReturnsProperGainers() { } @Test - void getLosersActuallyReturnsProperLosers() { - Stock teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200.00")); - Stock pearStock = new Stock("PEAR", "Pear inc.", new BigDecimal("97.00")); + void getGainersDoesNotReturnLoser() { + appleStock.addNewSalesPrice(new BigDecimal("150.00")); + teslaStock.addNewSalesPrice(new BigDecimal("230.00")); + pearStock.addNewSalesPrice(new BigDecimal("20")); + List actualGainers = testExchange.getGainers(3); + + assertFalse(actualGainers.contains(pearStock)); + } + + @Test + void getLosersActuallyReturnsProperLosers() { appleStock.addNewSalesPrice(new BigDecimal("50.00")); teslaStock.addNewSalesPrice(new BigDecimal("170.00")); - pearStock.addNewSalesPrice(new BigDecimal("82.00")); - - Exchange exchange = new Exchange("Exchange", List.of(appleStock, teslaStock, pearStock)); + pearStock.addNewSalesPrice(new BigDecimal("87")); - List actualLosers = exchange.getLosers(2); + List actualLosers = testExchange.getLosers(2); boolean actualLosersContainsValidLosers = actualLosers.contains(teslaStock) && actualLosers.contains(appleStock); @@ -233,18 +329,27 @@ void getLosersActuallyReturnsProperLosers() { } @Test - void invalidLimitForGettingGainersThrowError() { - Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + void getLosersDoesNotReturnWinners() { + + appleStock.addNewSalesPrice(new BigDecimal("50.00")); + teslaStock.addNewSalesPrice(new BigDecimal("170.00")); + pearStock.addNewSalesPrice(new BigDecimal("200")); - assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(0)); - assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-1)); + List actualLosers = testExchange.getLosers(3); + + boolean actualLosersNotContainsWinner = !actualLosers.contains(pearStock); + assertTrue(actualLosersNotContainsWinner); } @Test - void invalidLimitForGettingLosersThrowError() { - Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + void invalidLimitForGettingGainersThrowError() { + assertThrows(IllegalArgumentException.class, () -> testExchange.getGainers(0)); + assertThrows(IllegalArgumentException.class, () -> testExchange.getGainers(-1)); + } - assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(0)); - assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1)); + @Test + void invalidLimitForGettingLosersThrowError() { + assertThrows(IllegalArgumentException.class, () -> testExchange.getLosers(0)); + assertThrows(IllegalArgumentException.class, () -> testExchange.getLosers(-1)); } } From dccb4f8cdac9d5869168b5a3eda290ae37817b03 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 15:14:15 +0200 Subject: [PATCH 04/31] Feat: Refactored transactionarchive + testing Replaced for loops with streams, centralized concrete getters via get transactions, and added parameter guarding. Also updated test class. --- .../g40/mappe/engine/TransactionArchive.java | 78 ++++--- .../mappe/engine/TransactionArchiveTest.java | 202 +++++++++++------- 2 files changed, 162 insertions(+), 118 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java index dadc496..b7b9cf5 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java @@ -3,11 +3,15 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; +import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** - * Stores completed transactions. + * Stores completed transactions in an {@link ArrayList} object. + * + * @version 1.1.0 */ public final class TransactionArchive { @@ -20,6 +24,7 @@ public final class TransactionArchive { * Creates an empty transaction archive. */ public TransactionArchive() { + // Empty constructor. } /** @@ -28,8 +33,13 @@ public TransactionArchive() { * @param transaction the transaction to add * * @return true if the transaction was added + * + * @throws IllegalArgumentException if transaction is null */ public boolean add(final Transaction transaction) { + if (transaction == null) { + throw new IllegalArgumentException("Transaction cannot be null!"); + } return transactions.add(transaction); } @@ -48,24 +58,27 @@ public boolean isEmpty() { * @param week the week number * * @return list of transactions from the given week + * + * @throws IllegalArgumentException if week is less than 1. */ public List getTransactions(final int week) { - List result = new ArrayList<>(); - for (Transaction transaction : transactions) { - if (transaction.getWeek() == week) { - result.add(transaction); - } + if (!Validator.VALID_WEEK.isValid(Integer.toString(week))) { + throw new IllegalArgumentException( + Validator.VALID_WEEK.getErrorMessage() + ); } - return result; + return transactions.stream() + .filter(transaction -> transaction.getWeek() == week) + .toList(); } /** - * Returns all transactions. + * Returns an un-mutable reference to the transactions list. * - * @return list of transactions from the given week + * @return unmodifiable version of list. */ public List getTransactions() { - return transactions; + return Collections.unmodifiableList(transactions); } /** @@ -74,16 +87,15 @@ public List getTransactions() { * @param week the week number * * @return list of purchases from the given week + * + * @throws IllegalArgumentException if week is less than 1. */ - public List getPurchases(final int week) { - List result = new ArrayList<>(); - for (Transaction transaction : transactions) { - if (transaction instanceof Purchase purchase - && transaction.getWeek() == week) { - result.add(purchase); - } - } - return result; + public List getPurchases(final int week) + throws IllegalArgumentException { + return getTransactions(week).stream() + .filter(Purchase.class::isInstance) + .map(Purchase.class::cast) + .toList(); } /** @@ -92,15 +104,15 @@ public List getPurchases(final int week) { * @param week the week number * * @return list of sales from the given week + * + * @throws IllegalArgumentException if week is less than 1. */ - public List getSales(final int week) { - List result = new ArrayList<>(); - for (Transaction transaction : transactions) { - if (transaction instanceof Sale sale && transaction.getWeek() == week) { - result.add(sale); - } - } - return result; + public List getSales(final int week) + throws IllegalArgumentException { + return getTransactions(week).stream() + .filter(Sale.class::isInstance) + .map(Sale.class::cast) + .toList(); } /** @@ -109,13 +121,9 @@ public List getSales(final int week) { * @return number of distinct weeks */ public int countDistinctWeeks() { - List weeks = new ArrayList<>(); - for (Transaction transaction : transactions) { - int week = transaction.getWeek(); - if (!weeks.contains(week)) { - weeks.add(week); - } - } - return weeks.size(); + return (int) transactions.stream() + .map(Transaction::getWeek) + .distinct() + .count(); } } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java index 091daeb..78ffaf7 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java @@ -1,141 +1,177 @@ package edu.ntnu.idi.idatt2003.g40.mappe.engine; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; +import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import java.math.BigDecimal; import java.util.List; - -import edu.ntnu.idi.idatt2003.g40.mappe.model.*; -import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class TransactionArchiveTest { /** - * Transaction calculator to be used for testing. + * Test transactionArchive. + * */ + private TransactionArchive transactionArchive; + + /** + * Sample share used to emulate transactions. + * */ + private Share sampleShare; + + /** + * Sample sale calculator to calculate sales. + * */ + private SaleCalculator saleCalculator; + + /** + * Sample purchase calculater used to calculate purchases. * */ - private final TransactionCalculator calculator = new TransactionCalculator() { - @Override - public BigDecimal calculateGross() { - return BigDecimal.ZERO; - } - - @Override - public BigDecimal calculateCommission() { - return BigDecimal.ZERO; - } - - @Override - public BigDecimal calculateTax() { - return BigDecimal.ZERO; - } - - @Override - public BigDecimal calculateTotal() { - return BigDecimal.ZERO; - } - }; + private PurchaseCalculator purchaseCalculator; + + @BeforeEach + void setUp() { + transactionArchive = new TransactionArchive(); + Stock sampleStock = new Stock("AAPL", "Apple", new BigDecimal("100.00")); + sampleShare = new Share(sampleStock, + new BigDecimal("1.00"), sampleStock.getSalesPrice()); + saleCalculator = new SaleCalculator(sampleShare); + purchaseCalculator = new PurchaseCalculator(sampleShare); + } @Test void newArchiveIsEmpty() { - TransactionArchive archive = new TransactionArchive(); - - assertTrue(archive.isEmpty()); + assertTrue(transactionArchive.isEmpty()); } @Test - void addMakesArchiveNonEmpty() { - TransactionArchive archive = new TransactionArchive(); - Transaction transaction = createPurchase("AAPL", "Apple", 1); + void addValidTransactionMakesArchiveNonEmpty() { + Transaction purchase = createPurchase(1); - boolean result = archive.add(transaction); + boolean result = transactionArchive.add(purchase); assertTrue(result); - assertFalse(archive.isEmpty()); + assertFalse(transactionArchive.isEmpty()); } @Test - void getTransactionsReturnsOnlyTransactionsFromGivenWeek() { - TransactionArchive archive = new TransactionArchive(); + void addNullTransactionThrowsException() { + Transaction purchase = createPurchase(1); + assertDoesNotThrow(() -> transactionArchive.add(purchase)); + assertThrows(IllegalArgumentException.class, + () -> transactionArchive.add(null)); + } + + @Test + void genericGetTransactionsGetsAllTransactions() { + Transaction week1Purchase1 = createPurchase(1); + Transaction week1Purchase2 = createPurchase(1); + Transaction week2Purchase = createPurchase(2); + Transaction week4Sale = createPurchase(4); + + transactionArchive.add(week1Purchase1); + transactionArchive.add(week1Purchase2); + transactionArchive.add(week2Purchase); + transactionArchive.add(week4Sale); + + List result = transactionArchive.getTransactions(); + + assertEquals(4, result.size()); + assertTrue(result.contains(week1Purchase1)); + assertTrue(result.contains(week1Purchase2)); + assertTrue(result.contains(week2Purchase)); + assertTrue(result.contains(week4Sale)); + } - Transaction transaction1 = createPurchase("AAPL", "Apple", 1); - Transaction transaction2 = createSale("TSLA", "Tesla", 2); - Transaction transaction3 = createPurchase("NVDA", "Nvidia", 1); + @Test + void getTransactionsWithWeekSpecifierReturnsOnlyTransactionsFromGivenWeek() { + Transaction week1Purchase1 = createPurchase(1); + Transaction week1Purchase2 = createPurchase(1); + Transaction week2Purchase = createPurchase(2); - archive.add(transaction1); - archive.add(transaction2); - archive.add(transaction3); + transactionArchive.add(week1Purchase1); + transactionArchive.add(week1Purchase2); + transactionArchive.add(week2Purchase); - List result = archive.getTransactions(1); + List result = transactionArchive.getTransactions(1); assertEquals(2, result.size()); - assertTrue(result.contains(transaction1)); - assertTrue(result.contains(transaction3)); + assertTrue(result.contains(week1Purchase1)); + assertTrue(result.contains(week1Purchase2)); + assertFalse(result.contains(week2Purchase)); } @Test - void getPurchasesReturnsOnlyPurchasesFromGivenWeek() { - TransactionArchive archive = new TransactionArchive(); + void getTransactionsWithWeekSpecifierInvalidWeekThrowsException() { + assertDoesNotThrow(() -> transactionArchive.getTransactions(1)); + assertThrows(IllegalArgumentException.class, + () -> transactionArchive.getTransactions(0)); + } - Purchase purchase1 = createPurchase("AAPL", "Apple", 1); - Purchase purchase2 = createPurchase("NVDA", "Nvidia", 2); - Sale sale = createSale("TSLA", "Tesla", 1); + @Test + void getPurchasesReturnsOnlyPurchasesFromGivenWeek() { + Transaction week1Purchase = createPurchase(1); + Transaction week2Purchase = createPurchase(2); + Transaction week1Sale = createSale(1); - archive.add(purchase1); - archive.add(purchase2); - archive.add(sale); + transactionArchive.add(week1Purchase); + transactionArchive.add(week2Purchase); + transactionArchive.add(week1Sale); - List result = archive.getPurchases(1); + List result = transactionArchive.getPurchases(1); assertEquals(1, result.size()); - assertTrue(result.contains(purchase1)); + assertTrue(result.contains(week1Purchase)); } @Test void getSalesReturnsOnlySalesFromGivenWeek() { - TransactionArchive archive = new TransactionArchive(); + Transaction week1Sale = createSale(1); + Transaction week3Sale = createSale(3); + Transaction week1Purchase = createPurchase(1); - Sale sale1 = createSale("TSLA", "Tesla", 1); - Sale sale2 = createSale("NVDA", "Nvidia", 2); - Purchase purchase = createPurchase("AAPL", "Apple", 1); + transactionArchive.add(week1Sale); + transactionArchive.add(week3Sale); + transactionArchive.add(week1Purchase); - archive.add(sale1); - archive.add(sale2); - archive.add(purchase); - - List result = archive.getSales(1); + List result = transactionArchive.getSales(1); assertEquals(1, result.size()); - assertTrue(result.contains(sale1)); + assertTrue(result.contains(week1Sale)); } @Test void countDistinctWeeksCountsUniqueWeeksOnly() { - TransactionArchive archive = new TransactionArchive(); - - archive.add(createPurchase("AAPL", "Apple", 1)); - archive.add(createSale("TSLA", "Tesla", 1)); - archive.add(createPurchase("NVDA", "Nvidia", 2)); - archive.add(createSale("META", "Meta", 3)); + transactionArchive.add(createPurchase(1)); + transactionArchive.add(createPurchase(1)); + transactionArchive.add(createPurchase(2)); + transactionArchive.add(createSale(3)); - assertEquals(3, archive.countDistinctWeeks()); + assertEquals(3, transactionArchive.countDistinctWeeks()); } - private Purchase createPurchase(final String symbol, - final String company, - final int week) { - Stock stock = new Stock(symbol, company, new BigDecimal("100")); - Share share = new Share(stock, BigDecimal.ONE, new BigDecimal("100")); - return new Purchase(share, week, calculator); + private Transaction createPurchase(int week) { + return TransactionFactory.createTransaction( + TransactionType.PURCHASE, sampleShare, week, purchaseCalculator + ); } - private Sale createSale(final String symbol, - final String company, - final int week) { - Stock stock = new Stock(symbol, company, new BigDecimal("100")); - Share share = new Share(stock, BigDecimal.ONE, new BigDecimal("100")); - return new Sale(share, week, calculator); + private Transaction createSale(int week) { + return TransactionFactory.createTransaction( + TransactionType.SALE, sampleShare, week, saleCalculator + ); } } From e75440ca13a272decdb05e44258d5d1a0f8e150b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 15:15:09 +0200 Subject: [PATCH 05/31] Feat: EventManager refactor + test Added validation for adding subscribers, and refactored test class for eventmanager. Removed unused getname method for eventchannel, and added validator ruleset for valid week. --- .../g40/mappe/service/event/EventChannel.java | 7 - .../g40/mappe/service/event/EventManager.java | 19 +- .../g40/mappe/service/event/EventType.java | 8 - .../idatt2003/g40/mappe/utils/Validator.java | 6 + .../mappe/service/event/EventManagerTest.java | 185 +++++++++++++----- 5 files changed, 152 insertions(+), 73 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventChannel.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventChannel.java index e67e360..215547e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventChannel.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventChannel.java @@ -8,11 +8,4 @@ *

Decreases coupling and enables testing of event types.

* */ public interface EventChannel { - - /** - * Getter method for enum name. - * - * @return String name of enum. - * */ - String getName(); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManager.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManager.java index a4a2bad..79e4f7b 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManager.java @@ -32,9 +32,20 @@ public final class EventManager { * @param subscriber the {@link EventSubscriber} object to add. * @param eventChannel the {@link EventChannel} type this subscriber should subscribe to. * - * + * @throws IllegalArgumentException if subscriber is already subscribed to event channel, + * or if parameters are null. */ - public void addSubscriber(final EventSubscriber subscriber, final EventChannel eventChannel) { + public void addSubscriber(final EventSubscriber subscriber, final EventChannel eventChannel) + throws IllegalArgumentException { + if (subscriber == null || eventChannel == null) { + throw new IllegalArgumentException("Parameters cannot be null!"); + } + + List subscribers = subscriberMap.get(eventChannel); + if (subscribers != null && subscribers.contains(subscriber)) { + throw new IllegalArgumentException("Subscriber already subscribed to event channel!"); + } + subscriberMap.computeIfAbsent(eventChannel, k -> new ArrayList<>()).add(subscriber); } @@ -49,9 +60,9 @@ public void addSubscriber(final EventSubscriber subscriber, final EventChannel e */ public void invokeEvent(final EventData data) throws IllegalArgumentException { - if (data == null || !subscriberMap.containsKey(data.channel())) { + if (data == null || data.data() == null) { throw new IllegalArgumentException( - "No subscriber listening to this event!" + "Data cannot be null!" ); } else { for (EventSubscriber e : subscriberMap.get(data.channel())) { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java index ace4933..215ff37 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java @@ -51,12 +51,4 @@ public enum EventType implements EventChannel { * */ SELECT_STOCK_FOR_MINIGAME; - - /** - * {@inheritDoc} - * */ - @Override - public String getName() { - return this.name(); - } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java index b31108e..a9969db 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java @@ -63,6 +63,12 @@ public enum Validator { * */ VALID_POSITIVE_INT(VALID_INT.validationRule.and(s -> Integer.parseInt(s) >= 0), "Number is not positive!"), + + /** + * Rule that checks if a string represents a valid week. (Greater than 1). + * */ + VALID_WEEK(VALID_POSITIVE_INT.validationRule.and(s -> + Integer.parseInt(s) > 0), "Invalid week"), /** * Rule that checks if string is not empty, * and if it can be parsed into a {@link LocalDate} object. diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java index 716e96e..4852d1c 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java @@ -1,99 +1,176 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service.event; -import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewData; -import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class EventManagerTest { private enum TestEventTypes implements EventChannel { + /** + * Test event type 1. + * */ TEST_TYPE_1, + + /** + * Test event type 2. + * */ TEST_TYPE_2, + + /** + * Test event type 3 (Used by both subscribers). + * */ TEST_TYPE_3; - @Override - public String getName() { - return this.name(); - } } - private GenericEventPublisher testEventPublisher; - private GenericEventPublisher testEventPublisher2; - private GenericEventPublisher testEventPublisher3; - - private GenericEventSubscriber testEventSubscriber; - private GenericEventSubscriber testEventSubscriber2; - private EventManager testEventManager; + private SampleEventSubscriber sampleEventSubscriber1; + private SampleEventSubscriber sampleEventSubscriber2; @BeforeEach void setUp() { testEventManager = new EventManager(); - testEventSubscriber = new GenericEventSubscriber(); - testEventSubscriber2 = new GenericEventSubscriber(); + sampleEventSubscriber1 = new SampleEventSubscriber(); + sampleEventSubscriber2 = new SampleEventSubscriber(); + + testEventManager.addSubscriber(sampleEventSubscriber1, TestEventTypes.TEST_TYPE_1); + testEventManager.addSubscriber(sampleEventSubscriber1, TestEventTypes.TEST_TYPE_3); + + testEventManager.addSubscriber(sampleEventSubscriber2, TestEventTypes.TEST_TYPE_2); + testEventManager.addSubscriber(sampleEventSubscriber2, TestEventTypes.TEST_TYPE_3); + } - testEventPublisher = new GenericEventPublisher(testEventManager, TestEventTypes.TEST_TYPE_1); - testEventPublisher2 = new GenericEventPublisher(testEventManager, TestEventTypes.TEST_TYPE_2); - testEventPublisher3 = new GenericEventPublisher(testEventManager, TestEventTypes.TEST_TYPE_3); + @Test + void addingSubscriberNullParametersThrowsException() { + assertThrows(IllegalArgumentException.class, + () -> testEventManager.addSubscriber( + null, + TestEventTypes.TEST_TYPE_2)); + + assertThrows(IllegalArgumentException.class, + () -> testEventManager.addSubscriber( + sampleEventSubscriber1, + null)); + + // Will only throw exception if sample event subscriber 1 is duplicate. + assertDoesNotThrow( + () -> testEventManager.addSubscriber( + sampleEventSubscriber1, + TestEventTypes.TEST_TYPE_2)); - testEventManager.addSubscriber(testEventSubscriber, TestEventTypes.TEST_TYPE_1); - testEventManager.addSubscriber(testEventSubscriber2, TestEventTypes.TEST_TYPE_2); + } + + @Test + void addingDuplicateSubscriberForSameChannelThrowsException() { + assertDoesNotThrow( + () -> testEventManager.addSubscriber( + sampleEventSubscriber1, + TestEventTypes.TEST_TYPE_2)); + + assertThrows(IllegalArgumentException.class, + () -> testEventManager.addSubscriber( + sampleEventSubscriber1, + TestEventTypes.TEST_TYPE_1)); } @Test void firedEventCaughtByCorrectSubscriber() { - assertFalse(testEventSubscriber.invokedEvent); - testEventPublisher.fireEvent(); - assertTrue(testEventSubscriber.invokedEvent); + String dataToSend = "Data for type 1"; + + assertFalse(sampleEventSubscriber1.getInvoked()); + assertNull(sampleEventSubscriber1.getLastReceivedData()); + + EventData eventData = new EventData<>( + TestEventTypes.TEST_TYPE_1, + dataToSend + ); + testEventManager.invokeEvent(eventData); + + assertTrue(sampleEventSubscriber1.getInvoked()); + assertEquals(dataToSend, sampleEventSubscriber1.getLastReceivedData()); } @Test void firedEventNotCaughtByIncorrectSubscriber() { - assertFalse(testEventSubscriber.invokedEvent); - testEventPublisher2.fireEvent(); - assertFalse(testEventSubscriber.invokedEvent); + String dataToSend = "Data for type 1"; + + assertFalse(sampleEventSubscriber1.getInvoked()); + assertNull(sampleEventSubscriber1.getLastReceivedData()); + assertFalse(sampleEventSubscriber2.getInvoked()); + assertNull(sampleEventSubscriber2.getLastReceivedData()); + + EventData eventData = new EventData<>( + TestEventTypes.TEST_TYPE_2, + dataToSend + ); + testEventManager.invokeEvent(eventData); + + assertFalse(sampleEventSubscriber1.getInvoked()); + assertNull(sampleEventSubscriber1.getLastReceivedData()); + + assertTrue(sampleEventSubscriber2.getInvoked()); + assertEquals(dataToSend, sampleEventSubscriber2.getLastReceivedData()); } @Test - void firedEventThrowsErrorWhenNoSubscribers() { - assertFalse(testEventSubscriber.invokedEvent); - assertThrows(IllegalArgumentException.class, () -> { - testEventPublisher3.fireEvent(); - }); - assertFalse(testEventSubscriber.invokedEvent); - } + void firedEventsWithMultipleSubscribersCaughtByAllSubscribers() { + String dataToSend = "Data for type 1"; - private class GenericEventPublisher implements EventPublisher { + assertFalse(sampleEventSubscriber1.getInvoked()); + assertNull(sampleEventSubscriber1.getLastReceivedData()); + assertFalse(sampleEventSubscriber2.getInvoked()); + assertNull(sampleEventSubscriber2.getLastReceivedData()); - private final ViewData viewData; - private final EventData eventData; - private final EventManager eventManager; + EventData eventData = new EventData<>( + TestEventTypes.TEST_TYPE_3, + dataToSend + ); + testEventManager.invokeEvent(eventData); - public GenericEventPublisher(final EventManager eventManager, final TestEventTypes eventType) { - viewData = new ViewData(ViewEnum.IN_GAME); - eventData = new EventData(eventType, viewData); - this.eventManager = eventManager; - } + assertTrue(sampleEventSubscriber1.getInvoked()); + assertEquals(dataToSend, sampleEventSubscriber1.getLastReceivedData()); - public void fireEvent() { - invoke(eventData, eventManager); - } + assertTrue(sampleEventSubscriber2.getInvoked()); + assertEquals(dataToSend, sampleEventSubscriber2.getLastReceivedData()); + } - @Override - public void invoke(EventData data, EventManager eventManager) { - eventManager.invokeEvent(data); - } + @Test + void firedEventThrowsErrorWhenDataIsNull() { + + EventData invalidEventData = new EventData<>( + TestEventTypes.TEST_TYPE_1, + null + ); + + assertThrows(IllegalArgumentException.class, + () -> testEventManager.invokeEvent(null)); + + assertThrows(IllegalArgumentException.class, + () -> testEventManager.invokeEvent(invalidEventData)); } - private class GenericEventSubscriber implements EventSubscriber { - public boolean invokedEvent = false; + private static class SampleEventSubscriber implements EventSubscriber { + private boolean invokedEvent = false; + private Object lastReceivedData = null; @Override - public void handleEvent(EventData data) { + public void handleEvent(final EventData data) { invokedEvent = true; + lastReceivedData = data.data(); + } + + private boolean getInvoked() { + return invokedEvent; + } + + private Object getLastReceivedData() { + return lastReceivedData; } } } \ No newline at end of file From 7b3de6068f350b4f5dfd3481dd32f066f6796371 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 17:59:20 +0200 Subject: [PATCH 06/31] Feat: Changed how portfolio manages shares Previously, the portfolio held a list over multiple share objects. Now, it holds a map keyed with symbol and value is a share determined by the quantity of all shares of that symbol. This enables a more intuitive way of storing shares. --- .../idatt2003/g40/mappe/engine/Exchange.java | 75 +++------ .../idatt2003/g40/mappe/model/Portfolio.java | 133 +++++++++------- .../dashboard/DashBoardController.java | 8 +- .../g40/mappe/engine/ExchangeTest.java | 49 ++---- .../mappe/engine/TransactionArchiveTest.java | 22 +-- .../g40/mappe/model/PortfolioTest.java | 150 ++++++++++++------ .../mappe/service/TransactionFactoryTest.java | 61 +++---- 7 files changed, 244 insertions(+), 254 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index bc1ec00..c467434 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -4,9 +4,6 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; -import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; @@ -16,7 +13,7 @@ import java.util.List; import java.util.Map; import java.util.Random; -import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerWrapper; /** @@ -30,7 +27,7 @@ *

Advances week.

* * @see Player - * @see TransactionCalculator + * @see edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator * * @version 1.0.0 * */ @@ -98,12 +95,12 @@ public int getWeek() { } /** - * Getter method for the {@link IntegerProperty} object of week. + * Getter method for the {@link ReadOnlyIntegerProperty} object of week. * * @return week. * */ - public IntegerProperty weekProperty() { - return week; + public ReadOnlyIntegerProperty weekProperty() { + return week.getReadOnlyProperty(); } /** @@ -177,9 +174,8 @@ public Transaction buy(final String symbol, } Stock stock = getStock(symbol); Share share = new Share(stock, quantity, stock.getSalesPrice()); - TransactionCalculator calculator = new PurchaseCalculator(share); Transaction purchase = TransactionFactory.createTransaction( - TransactionType.PURCHASE, share, getWeek(), calculator + TransactionType.PURCHASE, share, getWeek() ); player.handleTransaction(purchase); return purchase; @@ -200,9 +196,8 @@ public Transaction sell(final Share share, final Player player) if (share == null || player == null) { throw new IllegalArgumentException("Invalid sell!"); } - TransactionCalculator calculator = new SaleCalculator(share); Transaction sale = TransactionFactory.createTransaction( - TransactionType.SALE, share, getWeek(), calculator + TransactionType.SALE, share, getWeek() ); player.handleTransaction(sale); return sale; @@ -212,9 +207,7 @@ TransactionType.SALE, share, getWeek(), calculator * Method called when a player sells share, * defined by an amount instead of specific {@link Share} object. * - *

Might split shares into multiples to ensure proper selling. - * Uses the {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio} - * to split.

+ *

{@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio}

* * @param amount the amount of "shares" to sell. * @param stockSymbol the stock to sell shares in. @@ -227,58 +220,38 @@ TransactionType.SALE, share, getWeek(), calculator * or if player does not own any shares * of the given stock. * */ - public List sell(BigDecimal amount, + public Transaction sell(BigDecimal amount, final String stockSymbol, final Player player) throws IllegalArgumentException { if (amount == null - || player == null - || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) { - throw new IllegalArgumentException("Invalid sell!"); + || player == null + || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) { + throw new IllegalArgumentException("Invalid sell parameters!"); } - // Get all shares the player owns of the stock given in the parameter. - List sharesOfStock = player.getPortfolio().getShares().stream() - .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) - .toList(); + List matchingShares = player.getPortfolio().getShares(stockSymbol); - // Throws error if player does not own any shares of the given stock. - if (sharesOfStock.isEmpty()) { - throw new IllegalArgumentException("Player does not own" - + " any shares of this stock!"); + if (matchingShares.isEmpty()) { + throw new IllegalArgumentException("Player does not own any shares of this stock!"); } - // Gets the total quantity amount of shares owned of given stock. - BigDecimal totalOwned = player.getPortfolio() - .getTotalSharesBySymbol(stockSymbol); + Share ownedPosition = matchingShares.getFirst(); + BigDecimal totalOwned = ownedPosition.getQuantity(); - // If amount wanted to sell is greater than total owned, - // sells entire collection. if (amount.compareTo(totalOwned) > 0) { amount = totalOwned; } - ArrayList transactions = new ArrayList<>(); - BigDecimal remainingToSell = amount; - // For every share owned, sells if the quantity is less than - // or equal to the remaining amount of shares to sell. - // Splits share if the remaining amount to sell is less than share quantity. - for (Share share : sharesOfStock) { + Stock stock = ownedPosition.getStock(); + Share shareToSell = new Share(stock, amount, stock.getSalesPrice()); - BigDecimal shareQty = share.getQuantity(); + Transaction sale = TransactionFactory.createTransaction( + TransactionType.SALE, shareToSell, getWeek() + ); + player.handleTransaction(sale); - if (shareQty.compareTo(remainingToSell) <= 0) { - remainingToSell = remainingToSell.subtract(shareQty); - transactions.add(sell(share, player)); - } else { - Share newShare = player.getPortfolio().splitShare( - share, remainingToSell - ); - transactions.add(sell(newShare, player)); - break; - } - } - return transactions; + return sale; } /** diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index 0c7ea5e..333d2e2 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -1,13 +1,10 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; -import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; - import java.math.BigDecimal; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Objects; +import java.util.Map; /** * Represents a player's portfolio of shares. @@ -18,9 +15,9 @@ public final class Portfolio { /** - * List of shares. + * Map used to handle internal shares. * */ - private final List shares = new ArrayList<>(); + private final Map shares = new HashMap<>(); /** * Creates an empty portfolio. @@ -30,24 +27,41 @@ public Portfolio() { } /** - * Adds a share to the portfolio. + * Adds a share to the portfolio. If share already exists, merges shares. * * @param share the share to add * - * @return {@code true} if the share was added, {@code false} otherwise - * * @throws IllegalArgumentException if share is null. */ - public boolean addShare(final Share share) throws IllegalArgumentException { + public void addShare(final Share share) throws IllegalArgumentException { if (share == null) { throw new IllegalArgumentException("Invalid share!"); } - return shares.add(share); + String symbol = share.getStock().getSymbol().toUpperCase(); + + if (shares.containsKey(symbol)) { + Share existingShare = shares.get(symbol); + BigDecimal totalQuantity = + existingShare.getQuantity().add(share.getQuantity()); + + shares.put(symbol, + new Share( + share.getStock(), + totalQuantity, existingShare.getPurchasePrice() + ) + ); + } else { + shares.put(symbol, share); + } } /** * Removes a share from the portfolio. * + *

Uses the quantity value to deduct share amount from the map. + * If quantity to remove is equal to amount held, removes share entirely. + * If not, splits the share.

+ * * @param share the share to remove * * @return {@code true} if the share was removed, @@ -56,11 +70,37 @@ public boolean addShare(final Share share) throws IllegalArgumentException { * @throws IllegalArgumentException if share is null. * */ - public boolean removeShare(final Share share) throws IllegalArgumentException { + public boolean removeShare(final Share share) + throws IllegalArgumentException { if (share == null) { throw new IllegalArgumentException("Invalid share!"); } - return shares.remove(share); + String symbol = share.getStock().getSymbol().toUpperCase(); + if (!shares.containsKey(symbol)) { + return false; + } + + Share ownedShare = shares.get(symbol); + int comparison = ownedShare.getQuantity().compareTo(share.getQuantity()); + + if (comparison < 0) { + throw new IllegalArgumentException( + "Cannot remove more shares than are currently owned!"); + } else if (comparison == 0) { + + shares.remove(symbol); + } else { + BigDecimal remainingQuantity = + ownedShare.getQuantity().subtract(share.getQuantity()); + shares.put(symbol, + new Share( + share.getStock(), + remainingQuantity, + ownedShare.getPurchasePrice() + ) + ); + } + return true; } /** @@ -69,7 +109,7 @@ public boolean removeShare(final Share share) throws IllegalArgumentException { * @return a list of shares */ public List getShares() { - return List.copyOf(shares); + return List.copyOf(shares.values()); } /** @@ -81,14 +121,14 @@ public List getShares() { * * @throws IllegalArgumentException if symbol is invalid. */ - public List getShares(final String symbol) throws IllegalArgumentException { + public List getShares(final String symbol) + throws IllegalArgumentException { if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } - return shares.stream() - .filter(s -> symbol.equalsIgnoreCase(s.getStock().getSymbol())) - .toList(); + Share share = shares.get(symbol.toUpperCase()); + return share != null ? List.of(share) : List.of(); } /** @@ -101,11 +141,15 @@ public List getShares(final String symbol) throws IllegalArgumentExceptio * * @throws IllegalArgumentException if share is null. */ - public boolean contains(final Share share) throws IllegalArgumentException { + public boolean contains(final Share share) + throws IllegalArgumentException { if (share == null) { throw new IllegalArgumentException("Invalid share!"); } - return shares.contains(share); + String symbol = share.getStock().getSymbol().toUpperCase(); + Share owned = shares.get(symbol); + return owned != null + && owned.getQuantity().compareTo(share.getQuantity()) >= 0; } /** @@ -115,11 +159,11 @@ public boolean contains(final Share share) throws IllegalArgumentException { * @return the net worth. * */ public BigDecimal getNetWorth() { - BigDecimal netWorth = new BigDecimal("0"); - - for (Share s : shares) { - SaleCalculator calculator = new SaleCalculator(s); - netWorth = netWorth.add(calculator.calculateTotal()); + BigDecimal netWorth = BigDecimal.ZERO; + for (Share s : shares.values()) { + netWorth = netWorth.add( + s.getQuantity().multiply(s.getStock().getSalesPrice()) + ); } return netWorth; } @@ -127,37 +171,16 @@ public BigDecimal getNetWorth() { /** * Helper method to get total amount of shares owned in a specific stock. * - * @param symbol the symbol of the stock to check for shares. - * */ - public BigDecimal getTotalSharesBySymbol(final String symbol) { - return shares.stream() - .filter(s -> s.getStock().getSymbol().equals(symbol)) - .map(Share::getQuantity) - .reduce(BigDecimal.ZERO, BigDecimal::add); - } - - /** - * "Splits" a share in two pieces based on an amount. - * - * @param share the share to split. - * @param splitAmount the amount to split by. + * @param symbol the symbol of the stock to check for shares. * - * @return the split share from the original to the split amount. - * - * @throws IllegalArgumentException if share or split amount is invalid. + * @return BigDecimal representing total quantity of all + * shares of this symbol. * */ - public Share splitShare(final Share share, final BigDecimal splitAmount) - throws IllegalArgumentException { - if (!contains(share) || splitAmount.compareTo(share.getQuantity()) > 0) { - throw new IllegalArgumentException("Cannot split share!"); + public BigDecimal getTotalSharesBySymbol(final String symbol) { + if (symbol == null) { + return BigDecimal.ZERO; } - BigDecimal remainingAmount = share.getQuantity().subtract(splitAmount); - - Share newShare1 = new Share(share.getStock(), splitAmount, share.getPurchasePrice()); - Share newShare2 = new Share(share.getStock(), remainingAmount, share.getPurchasePrice()); - removeShare(share); - addShare(newShare1); - addShare(newShare2); - return newShare1; + Share share = shares.get(symbol.toUpperCase()); + return share != null ? share.getQuantity() : BigDecimal.ZERO; } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java index 9d31d0d..fa1333c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java @@ -149,15 +149,13 @@ protected void initInteractions() { getViewElement().setOnAction(DashBoardActions.SELL_SHARES, () -> { if (Validator.NOT_EMPTY.isValid(getViewElement().getQuantityInputField().getText()) && Float.parseFloat(getViewElement().getQuantityInputField().getText()) > 0) { - List transactions = exchange.sell( + Transaction sale = exchange.sell( new BigDecimal(getViewElement().getQuantityInputField().getText()), getViewElement().getCurrentStock().getSymbol(), player); - for (Transaction t : transactions) { - if(t.isCommited()) { - getViewElement().addOwnedShares(-t.getShare().getQuantity().floatValue()); - } + if(sale.isCommited()) { + getViewElement().addOwnedShares(-sale.getShare().getQuantity().floatValue()); } } }); diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java index 147810b..66ce0c5 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java @@ -187,44 +187,18 @@ void sellShareObjectThrowsExceptionOnIllegalArguments() { } @Test - void sellingPartialSharesUpdatesPlayerMoneyAndPortfolioCorrectly() { + void sellingSharesBasedOnAmountFunctionsAsIntended() { testExchange.buy(appleStock.getSymbol(), new BigDecimal("3"), testPlayer); - List sales = - testExchange.sell(new BigDecimal("1.5"), appleStock.getSymbol(), testPlayer); - - assertEquals(1, sales.size()); + Transaction sale = testExchange.sell(new BigDecimal("1.5"), appleStock.getSymbol(), testPlayer); + assertInstanceOf(Sale.class, sale); BigDecimal expectedPlayerMoney = new BigDecimal("847.000"); assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + BigDecimal actualQuantity = testPlayer.getPortfolio().getShares(appleStock.getSymbol()).getFirst().getQuantity(); BigDecimal expectedRemainingShares = new BigDecimal("1.5"); - assertEquals(expectedRemainingShares, - testPlayer - .getPortfolio() - .getShares(appleStock.getSymbol()) - .getFirst() - .getQuantity() - ); - } - - @Test - void sellingPartialAmountOfSharesRequiringSplitSplitsSharesCorrectly() { - testExchange.buy(appleStock.getSymbol(), - new BigDecimal("3.12"), testPlayer); - testExchange.buy(appleStock.getSymbol(), - new BigDecimal("4.56"), testPlayer); - - List sales = - testExchange.sell(new BigDecimal("6.00"), - appleStock.getSymbol(), testPlayer); - - assertEquals(2, sales.size()); - assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); - assertEquals(new BigDecimal("2.88"), sales.get(1).getShare().getQuantity()); - - BigDecimal expectedPlayerMoney = new BigDecimal("822.16000"); - assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + assertEquals(0, expectedRemainingShares.compareTo(actualQuantity)); } @Test @@ -234,16 +208,13 @@ void attemptingToSellMoreSharesThanOwnedSellsAllSharesOwned() { testExchange.buy(appleStock.getSymbol(), new BigDecimal("4.56"), testPlayer); - List sales = - testExchange.sell(new BigDecimal("100.00"), - appleStock.getSymbol(), testPlayer); - assertEquals(2, sales.size()); - - assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); - assertEquals(new BigDecimal("4.56"), sales.get(1).getShare().getQuantity()); + testExchange.sell(new BigDecimal("100.00"), + appleStock.getSymbol(), testPlayer); BigDecimal expectedPlayerMoney = new BigDecimal("988.48000"); - assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + assertEquals(0, expectedPlayerMoney.compareTo(testPlayer.getMoney())); + + assertTrue(testPlayer.getPortfolio().getShares(appleStock.getSymbol()).isEmpty()); } @Test diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java index 78ffaf7..fce1960 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchiveTest.java @@ -11,8 +11,6 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import java.math.BigDecimal; @@ -32,24 +30,12 @@ final class TransactionArchiveTest { * */ private Share sampleShare; - /** - * Sample sale calculator to calculate sales. - * */ - private SaleCalculator saleCalculator; - - /** - * Sample purchase calculater used to calculate purchases. - * */ - private PurchaseCalculator purchaseCalculator; - @BeforeEach void setUp() { transactionArchive = new TransactionArchive(); Stock sampleStock = new Stock("AAPL", "Apple", new BigDecimal("100.00")); sampleShare = new Share(sampleStock, new BigDecimal("1.00"), sampleStock.getSalesPrice()); - saleCalculator = new SaleCalculator(sampleShare); - purchaseCalculator = new PurchaseCalculator(sampleShare); } @Test @@ -163,15 +149,15 @@ void countDistinctWeeksCountsUniqueWeeksOnly() { assertEquals(3, transactionArchive.countDistinctWeeks()); } - private Transaction createPurchase(int week) { + private Transaction createPurchase(final int week) { return TransactionFactory.createTransaction( - TransactionType.PURCHASE, sampleShare, week, purchaseCalculator + TransactionType.PURCHASE, sampleShare, week ); } - private Transaction createSale(int week) { + private Transaction createSale(final int week) { return TransactionFactory.createTransaction( - TransactionType.SALE, sampleShare, week, saleCalculator + TransactionType.SALE, sampleShare, week ); } } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java index 470f26a..781e06f 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java @@ -1,104 +1,152 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.util.List; -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class PortfolioTest { + /** + * Test portfolio used. + * */ + private Portfolio testPortfolio; + + /** + * Test stock used. + * */ + private Stock testStock; + + /** + * Test share used. + * */ + private Share testShare; + + @BeforeEach + void setUp() { + testPortfolio = new Portfolio(); + testStock = new Stock("AAPL", "Apple", new BigDecimal("150")); + testShare = new Share(testStock, new BigDecimal("2"), new BigDecimal("100")); + } + @Test void addShareAddsShareToPortfolio() { - Portfolio portfolio = new Portfolio(); - Stock stock = new Stock("AAPL", "Apple", new BigDecimal("150")); - Share share = new Share(stock, new BigDecimal("2"), new BigDecimal("100")); + assertDoesNotThrow( + () -> testPortfolio.addShare(testShare)); - boolean result = portfolio.addShare(share); + assertTrue(testPortfolio.contains(testShare)); + } - assertTrue(result); - assertTrue(portfolio.contains(share)); + @Test + void addingNullShareThrowsException() { + assertThrows(IllegalArgumentException.class, + () -> testPortfolio.addShare(null)); + + assertFalse(testPortfolio.contains(testShare)); } @Test void removeShareRemovesShareFromPortfolio() { - Portfolio portfolio = new Portfolio(); - Stock stock = new Stock("TSLA", "Tesla", new BigDecimal("200")); - Share share = new Share(stock, new BigDecimal("1"), new BigDecimal("200")); + assertFalse(testPortfolio.contains(testShare)); + + testPortfolio.addShare(testShare); - portfolio.addShare(share); - boolean result = portfolio.removeShare(share); + assertTrue(testPortfolio.contains(testShare)); + + boolean result = testPortfolio.removeShare(testShare); assertTrue(result); - assertFalse(portfolio.contains(share)); + assertFalse(testPortfolio.contains(testShare)); } @Test - void getSharesReturnsAllShares() { - Portfolio portfolio = new Portfolio(); + void removingNullShareThrowsException() { + assertThrows(IllegalArgumentException.class, + () -> testPortfolio.removeShare(null)); + } - Stock stock = new Stock("AAPL", "Apple", new BigDecimal("150")); - Share share = new Share(stock, new BigDecimal("3"), new BigDecimal("150")); + @Test + void removingMoreSharesThanOwnedThrowsException() { + testPortfolio.addShare(testShare); + Share testShare2 = new Share(testStock, new BigDecimal("4"), testStock.getSalesPrice()); + assertThrows(IllegalArgumentException.class, + () -> testPortfolio.removeShare(testShare2) + ); + } - portfolio.addShare(share); + @Test + void getSharesReturnsAllShares() { + testPortfolio.addShare(testShare); - List shares = portfolio.getShares(); + List shares = testPortfolio.getShares(); assertEquals(1, shares.size()); - assertTrue(shares.contains(share)); + assertTrue(shares.contains(testShare)); } @Test void getSharesWithSymbolReturnsMatchingShares() { - Portfolio portfolio = new Portfolio(); + Stock testStock2 = new Stock("TSLA", "Tesla", new BigDecimal("200")); - Stock apple = new Stock("AAPL", "Apple", new BigDecimal("150")); - Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200")); - - Share appleShare = new Share(apple, - new BigDecimal("1"), - new BigDecimal("150")); - Share teslaShare = new Share(tesla, + Share testShare2 = new Share(testStock2, new BigDecimal("1"), new BigDecimal("200")); - portfolio.addShare(appleShare); - portfolio.addShare(teslaShare); + testPortfolio.addShare(testShare); + testPortfolio.addShare(testShare2); - List result = portfolio.getShares("AAPL"); + List result = testPortfolio.getShares("AAPL"); assertEquals(1, result.size()); - assertTrue(result.contains(appleShare)); + assertTrue(result.contains(testShare)); } @Test - void containsReturnsFalseWhenShareNotPresent() { - Portfolio portfolio = new Portfolio(); - - Stock stock = new Stock("NVDA", "Nvidia", new BigDecimal("800")); - Share share = new Share(stock, new BigDecimal("1"), new BigDecimal("800")); - - assertFalse(portfolio.contains(share)); + void getSharesWithSymbolThrowsExceptionOnIllegalArgument() { + assertThrows(IllegalArgumentException.class, + () -> testPortfolio.getShares(null) + ); } @Test - void getNetWorthReturnsNetWorth() { - Portfolio portfolio = new Portfolio(); - - Stock stock = new Stock("NVDA", "Nvidia", new BigDecimal("800")); - Share share = new Share(stock, new BigDecimal("1"), new BigDecimal("800")); - portfolio.addShare(share); - - SaleCalculator saleCalculator = new SaleCalculator(share); - - BigDecimal calculatedNetWorth = saleCalculator.calculateTotal(); + void containsReturnsFalseWhenShareNotPresent() { + assertFalse(testPortfolio.contains(testShare)); + } - BigDecimal actualNetWorth = portfolio.getNetWorth(); + @Test + void containsThrowsExceptionOnIllegalArgument() { + assertThrows(IllegalArgumentException.class, + () -> testPortfolio.contains(null) + ); + } - assertEquals(calculatedNetWorth, actualNetWorth); + @Test + void getTotalSharesBySymbolReturnsCorrectValues() { + assertEquals(0, + BigDecimal.ZERO.compareTo( + testPortfolio.getTotalSharesBySymbol("AAPL") + ) + ); + + assertEquals(0, + BigDecimal.ZERO.compareTo( + testPortfolio.getTotalSharesBySymbol(null) + ) + ); + + testPortfolio.addShare(testShare); + + assertEquals(0, + new BigDecimal("2.0").compareTo( + testPortfolio.getTotalSharesBySymbol("AAPL") + ) + ); } } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactoryTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactoryTest.java index 6b25c35..1f49c32 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactoryTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactoryTest.java @@ -1,7 +1,9 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; @@ -15,56 +17,45 @@ class TransactionFactoryTest { private Stock testStock; private Share testShare; - private Sale testSale; - private Purchase testPurchase; - private SaleCalculator testSaleCalculator; - private PurchaseCalculator testPurchaseCalculator; @BeforeEach void setUp() { testStock = new Stock("AAPL", "APPLE INC.", new BigDecimal("100.00")); testShare = new Share(testStock, new BigDecimal("10.0"), testStock.getSalesPrice()); - testSaleCalculator = new SaleCalculator(testShare); - testSale = new Sale(testShare, 1, testSaleCalculator); - testPurchaseCalculator = new PurchaseCalculator(testShare); - testPurchase = new Purchase(testShare, 1, testPurchaseCalculator); } @Test void factoryReturnsCorrectSale() { - Transaction sale2 = TransactionFactory.createTransaction(TransactionType.SALE, testShare, 1, testSaleCalculator); - assertTrue(equalTransactions(testSale, sale2)); + int targetWeek = 1; + Transaction transaction = TransactionFactory.createTransaction(TransactionType.SALE, testShare, targetWeek); + assertNotNull(transaction); + assertInstanceOf(Sale.class, transaction); + assertEquals(targetWeek, transaction.getWeek()); + assertEquals(testShare, transaction.getShare()); + assertNotNull(transaction.getCalculator()); } @Test void factoryReturnsCorrectPurchase() { - Transaction purchase2 = TransactionFactory.createTransaction(TransactionType.PURCHASE, testShare, 1, testPurchaseCalculator); - assertTrue(equalTransactions(testPurchase, purchase2)); + int targetWeek = 1; + Transaction transaction = TransactionFactory.createTransaction(TransactionType.PURCHASE, testShare, targetWeek); + assertNotNull(transaction); + assertInstanceOf(Purchase.class, transaction); + assertEquals(targetWeek, transaction.getWeek()); + assertEquals(testShare, transaction.getShare()); + assertNotNull(transaction.getCalculator()); } @Test void factoryThrowsErrors() { - assertThrows(IllegalArgumentException.class, () -> { - TransactionFactory.createTransaction(TransactionType.PURCHASE, null, 1, testPurchaseCalculator); - }); - - assertThrows(IllegalArgumentException.class, () -> { - TransactionFactory.createTransaction(TransactionType.PURCHASE, testShare, -1, testPurchaseCalculator); - }); - - assertThrows(IllegalArgumentException.class, () -> { - TransactionFactory.createTransaction(TransactionType.PURCHASE, testShare, 1, null); - }); - - assertThrows(IllegalArgumentException.class, () -> { - TransactionFactory.createTransaction(null, testShare, 1, testPurchaseCalculator); - }); - } - - private boolean equalTransactions(final Transaction transaction1, final Transaction transaction2) { - return (transaction1.getWeek() == transaction2.getWeek() - && transaction1.getShare() == transaction2.getShare() - && transaction1.getCalculator() == transaction2.getCalculator() - && transaction1.isCommited() == transaction2.isCommited()); + assertThrows(IllegalArgumentException.class, + () -> TransactionFactory.createTransaction(TransactionType.PURCHASE, null, 1) + ); + assertThrows(IllegalArgumentException.class, + () -> TransactionFactory.createTransaction(TransactionType.PURCHASE, testShare, -1) + ); + assertThrows(IllegalArgumentException.class, + () -> TransactionFactory.createTransaction(null, testShare, 1) + ); } } \ No newline at end of file From f7aafc67613e0820a43939955b6d07a30a46885b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 17:59:32 +0200 Subject: [PATCH 07/31] Feat: Updated various classes. --- .../exceptions/NotEnoughMoneyException.java | 18 ++ .../idi/idatt2003/g40/mappe/model/Player.java | 128 +++++++++--- .../idatt2003/g40/mappe/model/SaveGame.java | 2 +- .../g40/mappe/service/TransactionFactory.java | 12 +- .../g40/mappe/model/PlayerStatusTest.java | 8 +- .../idatt2003/g40/mappe/model/PlayerTest.java | 186 +++++++++++++++--- 6 files changed, 283 insertions(+), 71 deletions(-) create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java new file mode 100644 index 0000000..8d19749 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/exceptions/NotEnoughMoneyException.java @@ -0,0 +1,18 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.exceptions; + +/** + * Exception primarily thrown when the active + * {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Player} object + * does not have enough money for a transaction to complete. + * */ +public class NotEnoughMoneyException extends RuntimeException { + + /** + * Constructor. + * + * @param message the exception message. + * */ + public NotEnoughMoneyException(final String message) { + super(message); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java index 9cac71e..61809ce 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java @@ -2,10 +2,11 @@ import edu.ntnu.idi.idatt2003.g40.mappe.controller.PlayerStatusController; import edu.ntnu.idi.idatt2003.g40.mappe.engine.TransactionArchive; +import edu.ntnu.idi.idatt2003.g40.mappe.exceptions.NotEnoughMoneyException; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.math.BigDecimal; -import javafx.beans.property.FloatProperty; -import javafx.beans.property.SimpleFloatProperty; +import javafx.beans.property.ReadOnlyFloatProperty; +import javafx.beans.property.ReadOnlyFloatWrapper; /** * Represents a player in the system. @@ -37,14 +38,18 @@ public final class Player { private BigDecimal money; /** - * Current net-worth of player as a listenable {@link FloatProperty} object. + * Current net-worth of player as a listenable, + * read-only, {@link ReadOnlyFloatWrapper} object. * */ - private final FloatProperty networthAsFloatProp = new SimpleFloatProperty(0); + private final ReadOnlyFloatWrapper networthAsFloatProp = + new ReadOnlyFloatWrapper(0f); /** - * Current money of player as a listenable {@link FloatProperty} object. + * Current money of player as a read-only + * {@link ReadOnlyFloatWrapper} object. * */ - private final FloatProperty moneyAsFloatProp = new SimpleFloatProperty(0); + private final ReadOnlyFloatWrapper moneyAsFloatProp + = new ReadOnlyFloatWrapper(0f); /** * The players' portfolio, holding their shares. @@ -63,19 +68,28 @@ public final class Player { * @param name the name of the player * @param startingMoney the starting amount of money * - * @throws IllegalArgumentException if name is null. + * @throws IllegalArgumentException if name is empty, + * or starting money is null, + * zero or negative. */ - public Player(final String name, final BigDecimal startingMoney) throws IllegalArgumentException { + public Player(final String name, + final BigDecimal startingMoney) + throws IllegalArgumentException { if (!Validator.NOT_EMPTY.isValid(name)) { - throw new IllegalArgumentException("Invalid name!"); + throw new IllegalArgumentException("Player name cannot be empty!"); + } + if (startingMoney == null + || startingMoney.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Starting money cannot be null, zero, or negative!" + ); } this.name = name; this.startingMoney = startingMoney; this.money = this.startingMoney; - this.networthAsFloatProp.setValue(this.startingMoney); - this.moneyAsFloatProp.setValue(this.startingMoney); this.portfolio = new Portfolio(); this.transactionArchive = new TransactionArchive(); + updateObservableProperties(); } /** @@ -109,18 +123,42 @@ public BigDecimal getMoney() { * Adds money to the players balance. * * @param amount the amount to add + * + * @throws IllegalArgumentException if money to add is negative or zero. */ - public void addMoney(final BigDecimal amount) { + public void addMoney(final BigDecimal amount) + throws IllegalArgumentException { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Can only add positive values to player!" + ); + } money = money.add(amount); + updateObservableProperties(); } /** * Withdraws money from the players balance. * * @param amount the amount to withdraw + * + * @throws IllegalArgumentException if money to withdraw is negative or zero, + * or if amount is more than current money. */ - public void withdrawMoney(final BigDecimal amount) { + public void withdrawMoney(final BigDecimal amount) + throws IllegalArgumentException { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Amount to withdraw must be positive!" + ); + } + if (money.compareTo(amount) < 0) { + throw new IllegalArgumentException( + "Cannot withdraw more money than available balance!" + ); + } money = money.subtract(amount); + updateObservableProperties(); } /** @@ -152,21 +190,23 @@ public BigDecimal getNetWorth() { } /** - * Get net-worth as a {@link FloatProperty} object, allowing listening for changes. + * Get net-worth as a {@link ReadOnlyFloatProperty} object, + * allowing listening for changes. * - * @return FloatProperty. + * @return networth as an immutable value. * */ - public FloatProperty getNetWorthAsFloatProperty() { - return networthAsFloatProp; + public ReadOnlyFloatProperty getNetWorthAsFloatProperty() { + return networthAsFloatProp.getReadOnlyProperty(); } /** - * Get money as a {@link FloatProperty} object, allowing listening for changes. + * Get money as a {@link ReadOnlyFloatProperty} object, + * allowing listening for changes. * - * @return FloatProperty. + * @return money as an immutable value. * */ - public FloatProperty getMoneyAsFloatProperty() { - return moneyAsFloatProp; + public ReadOnlyFloatProperty getMoneyAsFloatProperty() { + return moneyAsFloatProp.getReadOnlyProperty(); } /** @@ -184,22 +224,46 @@ public PlayerStatus getStatus() { * Method for handling a transaction for the player. * * @param transaction the transaction to handle. + * + * @throws IllegalArgumentException if transaction is null. + * @throws NotEnoughMoneyException if player does not have enough + * money for the transaction. * */ - public void handleTransaction(final Transaction transaction) { + public void handleTransaction(final Transaction transaction) + throws IllegalArgumentException, NotEnoughMoneyException { + if (transaction == null) { + throw new IllegalArgumentException("Cannot handle null transaction!"); + } + if (transaction instanceof Purchase purchase) { - if (money.floatValue() > transaction.getCalculator().calculateTotal().floatValue()) { + BigDecimal totalCost = purchase.getCalculator().calculateTotal(); + if (this.money.compareTo(totalCost) < 0) { + throw new NotEnoughMoneyException("Not enough money for transaction!"); + } + } + + switch (transaction) { + case Purchase purchase -> { withdrawMoney(purchase.getCalculator().calculateTotal()); portfolio.addShare(purchase.getShare()); - transactionArchive.add(transaction); - transaction.commit(this); } - } else if (transaction instanceof Sale sale) { - addMoney(sale.getCalculator().calculateTotal()); - portfolio.removeShare(sale.getShare()); - transactionArchive.add(transaction); - transaction.commit(this); + case Sale sale -> { + addMoney(sale.getCalculator().calculateTotal()); + portfolio.removeShare(sale.getShare()); + } + default -> throw new IllegalStateException("Unexpected value: " + transaction); } - networthAsFloatProp.setValue(getNetWorth().floatValue()); - moneyAsFloatProp.setValue(money); + transactionArchive.add(transaction); + transaction.commit(this); + + updateObservableProperties(); + } + + /** + * Helper method to synchronize the listener values. + */ + private void updateObservableProperties() { + this.moneyAsFloatProp.setValue(this.money.floatValue()); + this.networthAsFloatProp.setValue(this.getNetWorth().floatValue()); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index ff00e77..d790f97 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -14,7 +14,7 @@ * expected to be loaded with the default bundled stock data file. *

*/ -public class SaveGame { +public final class SaveGame { /** Display name of the save. */ private final String name; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java index 733b9c5..490a93c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java @@ -30,19 +30,17 @@ private TransactionFactory() { public static Transaction createTransaction(final TransactionType transactionType, final Share share, - final int week, - final TransactionCalculator - calculator) + final int week) throws IllegalArgumentException { if (transactionType == null || share == null - || !Validator.VALID_POSITIVE_INT.isValid(Integer.toString(week)) - || calculator == null) { + || !Validator.VALID_WEEK.isValid(Integer.toString(week)) + ) { throw new IllegalArgumentException("Null or empty parameters for factory!"); } else { return switch (transactionType) { - case SALE -> new Sale(share, week, calculator); - case PURCHASE -> new Purchase(share, week, calculator); + case SALE -> new Sale(share, week, new SaleCalculator(share)); + case PURCHASE -> new Purchase(share, week, new PurchaseCalculator(share)); default -> throw new IllegalArgumentException("Invalid transaction type!"); }; diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java index 1765d46..d5952dc 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerStatusTest.java @@ -31,7 +31,7 @@ void getStatusAfterDoubleIncreaseReturnsBetterStatus() { @Test void gettingStatusWhenNegativeDifferenceReturnsWorstStatus() { - testPlayer.addMoney(new BigDecimal(-1000)); + testPlayer.withdrawMoney(new BigDecimal(1000)); assertEquals(PlayerStatus.NOOB, testPlayer.getStatus()); } @@ -40,10 +40,10 @@ void multipleChangesInValueCauseCorrectStatus() { testPlayer.addMoney(new BigDecimal(2000)); assertEquals(PlayerStatus.PRO, testPlayer.getStatus()); - testPlayer.addMoney(new BigDecimal(-1000)); + testPlayer.withdrawMoney(new BigDecimal(1000)); assertEquals(PlayerStatus.TRYHARD, testPlayer.getStatus()); - testPlayer.addMoney(new BigDecimal(-500)); + testPlayer.withdrawMoney(new BigDecimal(500)); assertEquals(PlayerStatus.GOOD, testPlayer.getStatus()); } -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java index 70dd13b..9ac2d64 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PlayerTest.java @@ -1,65 +1,197 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import edu.ntnu.idi.idatt2003.g40.mappe.exceptions.NotEnoughMoneyException; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionFactory; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; import java.math.BigDecimal; - -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class PlayerTest { + /** + * Test player to use. + * */ + private Player testPlayer; + + @BeforeEach + void setUp() { + testPlayer = new Player("Alice", new BigDecimal("1000")); + } + @Test void constructorSetsNameMoneyPortfolioAndArchive() { - Player player = new Player("Alice", new BigDecimal("1000")); + assertEquals("Alice", testPlayer.getName()); + assertEquals(new BigDecimal("1000"), testPlayer.getMoney()); + assertNotNull(testPlayer.getPortfolio()); + assertNotNull(testPlayer.getTransactionArchive()); + assertEquals(1000f, + testPlayer.getMoneyAsFloatProperty().floatValue()); + assertEquals(1000f, + testPlayer.getNetWorthAsFloatProperty().floatValue()); + } - assertEquals("Alice", player.getName()); - assertEquals(new BigDecimal("1000"), player.getMoney()); - assertNotNull(player.getPortfolio()); - assertNotNull(player.getTransactionArchive()); + @Test + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new Player("Bob", new BigDecimal("2000")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("", new BigDecimal("2000")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player(null, new BigDecimal("2000")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("Bob", null) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("Bob", new BigDecimal("0")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Player("Bob", new BigDecimal("-100")) + ); } @Test void addMoneyIncreasesBalance() { - Player player = new Player("Bob", new BigDecimal("500")); - - player.addMoney(new BigDecimal("200")); + testPlayer.addMoney(new BigDecimal("200")); + assertEquals(new BigDecimal("1200"), testPlayer.getMoney()); + } - assertEquals(new BigDecimal("700"), player.getMoney()); + @Test + void addMoneyThrowsExceptionWhenIllegalArguments() { + assertDoesNotThrow( + () -> testPlayer.addMoney(new BigDecimal("200")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.addMoney(null) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.addMoney(new BigDecimal("-10")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.addMoney(new BigDecimal("0")) + ); } @Test void withdrawMoneyDecreasesBalance() { - Player player = new Player("Charlie", new BigDecimal("500")); - - player.withdrawMoney(new BigDecimal("150")); + testPlayer.withdrawMoney(new BigDecimal("150")); + assertEquals(new BigDecimal("850"), testPlayer.getMoney()); + } - assertEquals(new BigDecimal("350"), player.getMoney()); + @Test + void withdrawMoneyThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> testPlayer.withdrawMoney(new BigDecimal("200")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(null) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(new BigDecimal("-10")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(new BigDecimal("0")) + ); + + assertThrows(IllegalArgumentException.class, + () -> testPlayer.withdrawMoney(new BigDecimal("99999")) + ); } @Test void addAndWithdrawMoneyUpdateBalanceCorrectly() { - Player player = new Player("Dana", new BigDecimal("1000")); + testPlayer.addMoney(new BigDecimal("250")); + testPlayer.withdrawMoney(new BigDecimal("300")); - player.addMoney(new BigDecimal("250")); - player.withdrawMoney(new BigDecimal("300")); - - assertEquals(new BigDecimal("950"), player.getMoney()); + assertEquals(new BigDecimal("950"), testPlayer.getMoney()); } @Test - void getNetWorthCalculatesCorrectly() { + void getNetWorthCalculatesCorrectlyForSales() { Stock stock = new Stock("AAPL", "Apple inc.,", new BigDecimal("100.00")); - Player player = new Player("Bob", new BigDecimal("900")); Share share = new Share(stock, new BigDecimal("1"), new BigDecimal("100.00")); - player.getPortfolio().addShare(share); - SaleCalculator saleCalculator = new SaleCalculator(share); + BigDecimal expectedNetWorth = testPlayer.getNetWorth(); + Transaction transaction = TransactionFactory.createTransaction(TransactionType.SALE, share, 1); + + testPlayer.handleTransaction(transaction); + + BigDecimal actualNetWorth = testPlayer.getNetWorth(); + expectedNetWorth = expectedNetWorth.add(transaction.getCalculator().calculateTotal()); + + assertEquals(expectedNetWorth, actualNetWorth); + assertEquals(actualNetWorth.floatValue(), + testPlayer.getNetWorthAsFloatProperty().floatValue()); + assertEquals(testPlayer.getMoney().floatValue(), + testPlayer.getMoneyAsFloatProperty().floatValue()); + } + + @Test + void getNetWorthCalculatesCorrectlyForPurchases() { + Stock stock = new Stock("AAPL", "Apple inc.,", new BigDecimal("100.00")); + Share share = new Share(stock, new BigDecimal("1"), stock.getSalesPrice()); + + BigDecimal expectedNetWorth = testPlayer.getNetWorth(); + Transaction transaction = TransactionFactory.createTransaction(TransactionType.PURCHASE, share, 1); + + testPlayer.handleTransaction(transaction); + + BigDecimal actualNetWorth = testPlayer.getNetWorth(); + expectedNetWorth = expectedNetWorth.subtract( + transaction.getCalculator().calculateCommission() + ); + + assertEquals(expectedNetWorth, actualNetWorth); + assertEquals(actualNetWorth.floatValue(), + testPlayer.getNetWorthAsFloatProperty().floatValue()); + assertEquals(testPlayer.getMoney().floatValue(), + testPlayer.getMoneyAsFloatProperty().floatValue()); + } + + @Test + void handleTransactionThrowsExceptionsOnIllegalArgumentsAndNotEnoughMoney() { + Stock stock = new Stock("AAPL", "Apple inc.,", new BigDecimal("100.00")); + Share share = new Share(stock, new BigDecimal("1"), stock.getSalesPrice()); + + Share expensiveShare = new Share(stock, new BigDecimal("999.99"), new BigDecimal("999.99")); + + Transaction validTransaction = TransactionFactory.createTransaction( + TransactionType.PURCHASE, share, 1 + ); + + Transaction tooExpensiveTransaction = TransactionFactory.createTransaction( + TransactionType.PURCHASE, expensiveShare, 1 + ); + + assertDoesNotThrow( + () -> testPlayer.handleTransaction(validTransaction) + ); - BigDecimal calculatedNetWorth = player.getMoney().add(saleCalculator.calculateTotal()); - BigDecimal actualNetWorth = player.getNetWorth(); + assertThrows(IllegalArgumentException.class, + () -> testPlayer.handleTransaction(null) + ); - assertEquals(calculatedNetWorth, actualNetWorth); + assertThrows(NotEnoughMoneyException.class, + () -> testPlayer.handleTransaction(tooExpensiveTransaction) + ); } } From de95bc3364eb737fa05f89138209c257c36763fc Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:48:30 +0200 Subject: [PATCH 08/31] Fix: Updated version number for some classes --- .../java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java | 2 +- .../java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java | 2 ++ .../java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index c467434..9cf5230 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -29,7 +29,7 @@ * @see Player * @see edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator * - * @version 1.0.0 + * @version 1.1.0 * */ public final class Exchange { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java index 61809ce..0b66b11 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java @@ -19,6 +19,8 @@ *
  • Has a set amount of money to use on said exchange.
  • *
  • Has a {@link TransactionArchive}
  • * + * + * @version 1.1.0 * */ public final class Player { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index 333d2e2..40a3afd 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -11,6 +11,8 @@ * *

    The portfolio stores shares and provides operations for adding, removing, * retrieving and checking ownership of shares.

    + * + * @version 1.1.0 */ public final class Portfolio { From 290fdab0a34cde5d661d82a99e20e1a12be59094 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:48:40 +0200 Subject: [PATCH 09/31] Feat: Validation for savegame --- .../edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index d790f97..e6c60ec 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -1,5 +1,7 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; + /** * Represents one save game entry. * @@ -44,6 +46,11 @@ public SaveGame(final String name, final double balance, final double startingCapital, final String stockDataPath) { + if (!Validator.NOT_EMPTY.isValid(name) + || balance <= 0 + || startingCapital <= 0) { + throw new IllegalArgumentException("Invalid Save configuration!"); + } this.name = name; this.balance = balance; this.startingCapital = startingCapital; From 706db1dc9f90b95ec00079f2847bfbb8b6e42667 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:48:50 +0200 Subject: [PATCH 10/31] Feat: Updated validation for share --- .../idi/idatt2003/g40/mappe/model/Share.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Share.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Share.java index 3328a74..30aba43 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Share.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Share.java @@ -32,13 +32,25 @@ public final class Share { * @param quantity the quantity purchased * @param purchasePrice the price per unit at purchase time * - * @throws IllegalArgumentException if stock is null. + * @throws IllegalArgumentException if parameters are null or invalid. */ public Share(final Stock stock, final BigDecimal quantity, final BigDecimal purchasePrice) throws IllegalArgumentException { - if (stock == null) { - throw new IllegalArgumentException("Invalid stock!"); + if (stock == null + || quantity == null + || purchasePrice == null) { + throw new IllegalArgumentException("Invalid share configuration!"); + } + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Quantity cannot be negative or zero!" + ); + } + if (purchasePrice.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException( + "Purchase price cannot be negative or zero!" + ); } this.stock = stock; this.quantity = quantity; From 1c015f89ca73082851390e8db914f64cfabbeb16 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:49:18 +0200 Subject: [PATCH 11/31] Feat: Updated validation for Stock --- .../idi/idatt2003/g40/mappe/model/Stock.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java index 0aff105..d7ff05a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java @@ -40,6 +40,8 @@ public final class Stock { * @param symbol the unique stock symbol * @param company the name of the company * @param salesPrice the initial sales price of the stock + * + * @throws IllegalArgumentException if parameters are null or invalid. */ public Stock(final String symbol, final String company, @@ -47,12 +49,17 @@ public Stock(final String symbol, if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( Validator.VALID_STOCK_SYMBOL.getErrorMessage()); - } else { - this.symbol = symbol; - this.company = company; - this.fortune = 0; - prices.add(salesPrice); } + if (!Validator.NOT_EMPTY.isValid(company)) { + throw new IllegalArgumentException(Validator.NOT_EMPTY.getErrorMessage()); + } + if (salesPrice == null || salesPrice.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Sales price of cannot be negative or zero!"); + } + this.symbol = symbol; + this.company = company; + this.fortune = 0; + prices.add(salesPrice); } /** @@ -109,11 +116,11 @@ public BigDecimal getSalesPrice() { */ public void addNewSalesPrice(final BigDecimal price) throws IllegalArgumentException { - if (price != null && price.intValue() != 0) { - prices.add(price); - } else { - throw new IllegalArgumentException("Invalid price to add to stock: " + getSymbol()); + if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Invalid price to add to stock: " + + getSymbol()); } + prices.add(price); } /** From ada261d0a0b130eb7b47bf5377641308f7486864 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:49:31 +0200 Subject: [PATCH 12/31] Feat: Updated validation for Transaction --- .../g40/mappe/model/Transaction.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Transaction.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Transaction.java index fe7e6c5..62993f1 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Transaction.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Transaction.java @@ -1,6 +1,7 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; /** * Transaction abstract class. @@ -40,14 +41,22 @@ public abstract class Transaction { protected Transaction(final Share share, final int week, final TransactionCalculator calculator) - throws IllegalArgumentException{ - if (share == null || calculator == null) { - throw new IllegalArgumentException("Invalid stock or calculator!"); - } else { - this.share = share; - this.week = week; - this.calculator = calculator; + throws IllegalArgumentException { + if (share == null + || calculator == null) { + throw new IllegalArgumentException( + "Invalid configuration for transaction!" + ); } + if (!Validator.VALID_WEEK.isValid(Integer.toString(week))) { + throw new IllegalArgumentException( + Validator.VALID_WEEK.getErrorMessage() + ); + } + this.share = share; + this.week = week; + this.calculator = calculator; + } /** From 5e9017ce68898d7883702cd9699788bcadc7b65a Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:49:42 +0200 Subject: [PATCH 13/31] Feat: Updated Validator Enum --- .../idatt2003/g40/mappe/utils/Validator.java | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java index a9969db..6e1830b 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/Validator.java @@ -1,7 +1,5 @@ package edu.ntnu.idi.idatt2003.g40.mappe.utils; -import java.time.LocalDate; -import java.time.format.DateTimeParseException; import java.util.function.Predicate; /** @@ -58,29 +56,11 @@ public enum Validator { VALID_STOCK_SYMBOL(NOT_EMPTY.validationRule.and(s -> s.length() == 4), "Invalid stock symbol!"), - /** - * Rule that checks if a string represents a positive integer. - * */ - VALID_POSITIVE_INT(VALID_INT.validationRule.and(s -> - Integer.parseInt(s) >= 0), "Number is not positive!"), - /** * Rule that checks if a string represents a valid week. (Greater than 1). * */ - VALID_WEEK(VALID_POSITIVE_INT.validationRule.and(s -> - Integer.parseInt(s) > 0), "Invalid week"), - /** - * Rule that checks if string is not empty, - * and if it can be parsed into a {@link LocalDate} object. - */ - VALID_DATE(NOT_EMPTY.validationRule.and(s -> { - try { - LocalDate.parse(s); - return true; - } catch (DateTimeParseException e) { - return false; - } - }), "Invalid Date!"); + VALID_WEEK(VALID_INT.validationRule.and(s -> + Integer.parseInt(s) > 0), "Invalid week"); /** The predicate field set when creating constants. */ private final Predicate validationRule; From e2a84b6e0fc1cffcfef5e0123ed7962e9e976481 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:20 +0200 Subject: [PATCH 14/31] Feat: removed OnUpdate for CreateGameView --- .../idatt2003/g40/mappe/view/creategame/CreateGameView.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java index a64bec2..48550ac 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java @@ -302,12 +302,6 @@ protected void initStyling() { createGameButton.setDisable(true); } - /** {@inheritDoc} */ - @Override - public void onUpdate() { - resetFields(); - } - /** * Refreshes the highlight on the two stock-source buttons so the * currently-active choice stands out from the inactive one. From a0fa3283953c23b739e9bfa9df991311b64e9c43 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:25 +0200 Subject: [PATCH 15/31] Update ViewElement.java --- .../idatt2003/g40/mappe/view/ViewElement.java | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java index 11e91cd..d0c2ef0 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java @@ -1,6 +1,5 @@ package edu.ntnu.idi.idatt2003.g40.mappe.view; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.util.EnumMap; import java.util.Map; @@ -47,14 +46,18 @@ public abstract class ViewElement> { * @param rootPane an instance of type T (defined in the class). * @param viewName The name of the view as an {@link ViewEnum}. * + * @throws IllegalArgumentException if parameters are invalid. + * */ - protected ViewElement(final T rootPane, final ViewEnum viewName, final Class actionEnum) { + protected ViewElement(final T rootPane, + final ViewEnum viewName, + final Class actionEnum) + throws IllegalArgumentException { this(rootPane, actionEnum); - if (Validator.NOT_EMPTY.isValid(viewName.name())) { - this.viewName = viewName; - } else { + if (!Validator.NOT_EMPTY.isValid(viewName.name())) { throw new IllegalArgumentException(Validator.NOT_EMPTY.getErrorMessage()); } + this.viewName = viewName; } /** @@ -62,16 +65,17 @@ protected ViewElement(final T rootPane, final ViewEnum viewName, final Class * * @param rootPane the root of this view. * + * @throws IllegalArgumentException if parameters are null. */ - protected ViewElement(final T rootPane, final Class actionEnum) { - if (rootPane != null) { - setRootPane(rootPane); - this.buttonMap = new EnumMap<>(actionEnum); - initLayout(); - initStyling(); - } else { + protected ViewElement(final T rootPane, final Class actionEnum) + throws IllegalArgumentException { + if (rootPane == null || actionEnum == null) { throw new IllegalArgumentException("Invalid ViewElement!"); } + setRootPane(rootPane); + this.buttonMap = new EnumMap<>(actionEnum); + initLayout(); + initStyling(); } /** @@ -155,21 +159,10 @@ public void setOnAction(final A action, final Runnable logic) } } - /** - * Method that defines how view elements set data. - * - * @param The type of data to set. - * @param data the data to set. - * - */ - public void setData(final T2 data) { - setViewName(data.getSceneName()); - } - /** * Method called when updating a view. * */ public void onUpdate() { - + // Empty by default. } } From 44ff6e3b51e7b321ea6c1cd819247d1cb8a72853 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:28 +0200 Subject: [PATCH 16/31] Update ViewManager.java --- .../idi/idatt2003/g40/mappe/view/ViewManager.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java index b3ce064..f6de33d 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java @@ -6,6 +6,8 @@ import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import java.util.EnumMap; import java.util.Map; + +import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import javafx.scene.Scene; import javafx.stage.Stage; @@ -108,6 +110,7 @@ public void setScene(final ViewElement viewElement) ); } currentView = viewElement; + viewElement.onUpdate(); } } @@ -128,12 +131,10 @@ public void setScene(final ViewElement viewElement) public void setScene(final ViewData data) throws IllegalArgumentException { if (data == null) { throw new IllegalArgumentException("Data is null!"); - } else { - ViewElement viewElement = viewMap.get(data.getSceneName()); - viewElement.setData(data); - setScene(viewElement); - currentView = viewElement; } + ViewElement viewElement = viewMap.get(data.getSceneName()); + setScene(viewElement); + currentView = viewElement; } /** From d34b9a23077505407e07b279c31c294ffc5ae17f Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:31 +0200 Subject: [PATCH 17/31] Update PurchaseTest.java --- .../g40/mappe/model/PurchaseTest.java | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PurchaseTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PurchaseTest.java index 42caf80..b2c03c6 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PurchaseTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PurchaseTest.java @@ -1,11 +1,14 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.math.BigDecimal; - import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** @@ -14,44 +17,70 @@ final class PurchaseTest { /** - * Valid test stock. + * Test purchase used in various tests. * */ - private final Stock testStock = - new Stock("AAPL", "Apple Inc.", new BigDecimal("100.00")); + private Purchase testPurchase; /** * Valid test share. * */ - private final Share testShare = - new Share(testStock, new BigDecimal("10"), new BigDecimal("10")); + private Share testShare; /** * Valid test purchase calculator. * */ - private final PurchaseCalculator testPurchaseCalculator = - new PurchaseCalculator(testShare); + private PurchaseCalculator testPurchaseCalculator; /** * Valid test player. * */ - private final Player testPlayer = - new Player("TestName", new BigDecimal("1000.00")); + private Player testPlayer; + + @BeforeEach + void setUp() { + Stock testStock = new Stock( + "AAPL", + "Apple Inc.", + new BigDecimal("100.00") + ); + testShare = new Share( + testStock, + new BigDecimal("10"), + new BigDecimal("10") + ); + testPurchaseCalculator = new PurchaseCalculator(testShare); + testPlayer = new Player("TestName", new BigDecimal("1000.00")); + testPurchase = new Purchase(testShare, 1, testPurchaseCalculator); + } @Test void constructorSetsValues() { - Purchase purchase = new Purchase(testShare, 1, testPurchaseCalculator); - - assertEquals(testShare, purchase.getShare()); - assertEquals(1, purchase.getWeek()); - assertEquals(testPurchaseCalculator, purchase.getCalculator()); + assertEquals(testShare, testPurchase.getShare()); + assertEquals(1, testPurchase.getWeek()); + assertEquals(testPurchaseCalculator, testPurchase.getCalculator()); } @Test - void commitMethodSetsCommitToTrue() { - Purchase purchase = new Purchase(testShare, 1, testPurchaseCalculator); + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new Purchase(testShare, 1, testPurchaseCalculator) + ); - purchase.commit(testPlayer); + assertThrows(IllegalArgumentException.class, + () -> new Purchase(null, 1, testPurchaseCalculator) + ); + assertThrows(IllegalArgumentException.class, + () -> new Purchase(testShare, 0, testPurchaseCalculator) + ); + assertThrows(IllegalArgumentException.class, + () -> new Purchase(testShare, 1, null) + ); + } - assertTrue(purchase.isCommited()); + @Test + void commitMethodSetsCommitToTrue() { + assertFalse(testPurchase.isCommited()); + testPurchase.commit(testPlayer); + assertTrue(testPurchase.isCommited()); } } From 76f784d4c65a77027289097dab9dd3b862be5069 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:35 +0200 Subject: [PATCH 18/31] Update SaleTest.java --- .../idatt2003/g40/mappe/model/SaleTest.java | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaleTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaleTest.java index 22e4e4e..feda7ac 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaleTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaleTest.java @@ -1,11 +1,14 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; import java.math.BigDecimal; - -import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** @@ -14,44 +17,70 @@ final class SaleTest { /** - * Valid test stock. + * Test sale used in various tests. * */ - private final Stock testStock = - new Stock("AAPL", "Apple Inc.", new BigDecimal("100.00")); + private Sale testSale; /** * Valid test share. * */ - private final Share testShare = - new Share(testStock, new BigDecimal("10"), new BigDecimal("10")); + private Share testShare; /** * Valid test purchase calculator. * */ - private final PurchaseCalculator testSaleCalculator = - new PurchaseCalculator(testShare); + private SaleCalculator testSaleCalculator; /** * Valid test player. * */ - private final Player testPlayer = - new Player("TestName", new BigDecimal("1000.00")); + private Player testPlayer; + + @BeforeEach + void setUp() { + Stock testStock = new Stock( + "AAPL", + "Apple Inc.", + new BigDecimal("100.00") + ); + testShare = new Share( + testStock, + new BigDecimal("10"), + new BigDecimal("10") + ); + testSaleCalculator = new SaleCalculator(testShare); + testPlayer = new Player("TestName", new BigDecimal("1000.00")); + testSale = new Sale(testShare, 1, testSaleCalculator); + } @Test void constructorSetsValues() { - Sale sale = new Sale(testShare, 1, testSaleCalculator); - - assertEquals(testShare, sale.getShare()); - assertEquals(1, sale.getWeek()); - assertEquals(testSaleCalculator, sale.getCalculator()); + assertEquals(testShare, testSale.getShare()); + assertEquals(1, testSale.getWeek()); + assertEquals(testSaleCalculator, testSale.getCalculator()); } @Test - void commitMethodSetsCommitToTrue() { - Sale sale = new Sale(testShare, 1, testSaleCalculator); + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new Purchase(testShare, 1, testSaleCalculator) + ); - sale.commit(testPlayer); + assertThrows(IllegalArgumentException.class, + () -> new Purchase(null, 1, testSaleCalculator) + ); + assertThrows(IllegalArgumentException.class, + () -> new Purchase(testShare, 0, testSaleCalculator) + ); + assertThrows(IllegalArgumentException.class, + () -> new Purchase(testShare, 1, null) + ); + } - assertTrue(sale.isCommited()); + @Test + void commitMethodSetsCommitToTrue() { + assertFalse(testSale.isCommited()); + testSale.commit(testPlayer); + assertTrue(testSale.isCommited()); } } From e6736910fb35b7b2cfc42c6cac41a6e7ec7ca218 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:37 +0200 Subject: [PATCH 19/31] Create SaveGameTest.java --- .../g40/mappe/model/SaveGameTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGameTest.java diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGameTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGameTest.java new file mode 100644 index 0000000..907431e --- /dev/null +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGameTest.java @@ -0,0 +1,53 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.model; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test class for {@link SaveGame}. + * */ +class SaveGameTest { + + /** + * {@link SaveGame} object to use for testing. + * */ + private SaveGame testSaveGame; + + @BeforeEach + void setUp() { + testSaveGame = new SaveGame("Save 1", 10, 100, "Stock path"); + } + + @Test + void constructorSetsValuesAsExpected() { + Assertions.assertEquals("Save 1", testSaveGame.getName()); + Assertions.assertEquals(10, testSaveGame.getBalance()); + Assertions.assertEquals(100, testSaveGame.getStartingCapital()); + Assertions.assertEquals("Stock path", testSaveGame.getStockDataPath()); + } + + @Test + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new SaveGame("Save 2", 10, 100, "Stock path 2") + ); + + assertThrows(IllegalArgumentException.class, + () -> new SaveGame("", 10, 100, "Stock path 2") + ); + + assertThrows(IllegalArgumentException.class, + () -> new SaveGame("Save 2", 0, 100, "Stock path 2") + ); + + assertThrows(IllegalArgumentException.class, + () -> new SaveGame("Save 2", 10, -10, "Stock path 2") + ); + } +} \ No newline at end of file From e6a3914431972e3dce24f0de212b1559229c8de2 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:40 +0200 Subject: [PATCH 20/31] Update ShareTest.java --- .../idatt2003/g40/mappe/model/ShareTest.java | 106 ++++++++++++++---- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/ShareTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/ShareTest.java index bb78ea1..f73a2e4 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/ShareTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/ShareTest.java @@ -1,52 +1,114 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class ShareTest { - @Test - void constructorStoresAllValuesCorrectly() { - Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00")); + /** + * Share to use for testing. + */ + private Share testShare; + + /** + * Stock to use for testing. + * */ + private Stock testStock; + + @BeforeEach + void setUp() { + testStock = new Stock( + "AAPL", + "Apple Inc.", + new BigDecimal("150.00") + ); BigDecimal quantity = new BigDecimal("10"); BigDecimal purchasePrice = new BigDecimal("145.50"); + testShare = new Share(testStock, quantity, purchasePrice); + } + + @Test + void constructorStoresAllValuesCorrectly() { + assertSame(testStock, testShare.getStock()); + + assertEquals(0, + new BigDecimal("10") + .compareTo(testShare.getQuantity()) + ); + + assertEquals(0, + new BigDecimal("145.50") + .compareTo(testShare.getPurchasePrice()) + ); + } + + @Test + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new Share(testStock, new BigDecimal("1"), + new BigDecimal("100")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Share(null, new BigDecimal("1"), new BigDecimal("100")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Share(testStock, null, new BigDecimal("100")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Share(testStock, new BigDecimal("1"), null) + ); - Share share = new Share(stock, quantity, purchasePrice); + assertThrows(IllegalArgumentException.class, + () -> new Share(testStock, new BigDecimal("0"), + new BigDecimal("100")) + ); - assertSame(stock, share.getStock()); - assertEquals(quantity, share.getQuantity()); - assertEquals(purchasePrice, share.getPurchasePrice()); + assertThrows(IllegalArgumentException.class, + () -> new Share(testStock, new BigDecimal("1"), new BigDecimal("0")) + ); } @Test void shareSupportsDecimalValues() { - Stock stock = new Stock("TSLA", "Tesla Inc.", new BigDecimal("200.00")); - Share share = new Share( - stock, + Share testShare2 = new Share( + testStock, new BigDecimal("2.5"), new BigDecimal("198.75") ); - assertEquals(new BigDecimal("2.5"), share.getQuantity()); - assertEquals(new BigDecimal("198.75"), share.getPurchasePrice()); + assertEquals(0, + new BigDecimal("2.5") + .compareTo(testShare2.getQuantity()) + ); + + assertEquals(0, + new BigDecimal("198.75") + .compareTo(testShare2.getPurchasePrice()) + ); } @Test void getStockReturnsCorrectStockObject() { - Stock stock = new Stock("NVDA", - "NVIDIA Corporation", - new BigDecimal("875.40")); - Share share = new Share(stock, - new BigDecimal("4"), - new BigDecimal("850.00")); - - assertSame(stock, share.getStock()); - assertEquals("NVDA", share.getStock().getSymbol()); - assertEquals("NVIDIA Corporation", share.getStock().getCompany()); + assertSame(testStock, testShare.getStock()); + assertEquals( + "AAPL", + testShare.getStock().getSymbol() + ); + + assertEquals( + "Apple Inc.", + testShare.getStock().getCompany() + ); } } From 98bdd9f63151085c600f7cdafd3eb6980aeed77c Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:42 +0200 Subject: [PATCH 21/31] Update StockTest.java --- .../idatt2003/g40/mappe/model/StockTest.java | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/StockTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/StockTest.java index c5f907d..8c7f48d 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/StockTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/StockTest.java @@ -1,15 +1,20 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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 java.math.BigDecimal; import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - final class StockTest { + /** + * Stock to use for testing. + * */ private Stock testStock; @BeforeEach @@ -23,6 +28,28 @@ void constructorSetsSymbolAndCompany() { assertEquals("Apple Inc.", testStock.getCompany()); } + @Test + void constructorThrowsExceptionOnIllegalArguments() { + assertDoesNotThrow( + () -> new Stock("AAPL", "APPLE INC.", new BigDecimal("100")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Stock("", "APPLE INC.", new BigDecimal("100")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Stock("AAPL", "", new BigDecimal("100")) + ); + + assertThrows(IllegalArgumentException.class, + () -> new Stock("AAPL", "APPLE INC.", new BigDecimal("0")) + ); + assertThrows(IllegalArgumentException.class, + () -> new Stock("AAPL", "APPLE INC.", null) + ); + } + @Test void addNewSalesPriceThenGetSalesPriceReturnsLastAddedPrice() { testStock.addNewSalesPrice(new BigDecimal("123.45")); @@ -39,11 +66,18 @@ void addNewSalesPriceTwiceGetSalesPriceReturnsMostRecent() { } @Test - void addNewSalesPriceDoesNotAllowNullCurrentImplementation() { - + void addNewSalesPriceThrowsExceptionOnIllegalArguments() { assertThrows(IllegalArgumentException.class, () -> { testStock.addNewSalesPrice(null); }); + + assertThrows(IllegalArgumentException.class, () -> { + testStock.addNewSalesPrice(new BigDecimal("0")); + }); + + assertThrows(IllegalArgumentException.class, () -> { + testStock.addNewSalesPrice(new BigDecimal("-10")); + }); } @Test From c174d6e52b3616c6b5c2e3a8eff820e5f1dc879c Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:44 +0200 Subject: [PATCH 22/31] Update ViewControllerTest.java --- .../g40/mappe/view/ViewControllerTest.java | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewControllerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewControllerTest.java index e105cdf..948d9ca 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewControllerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewControllerTest.java @@ -11,30 +11,40 @@ import org.testfx.framework.junit5.ApplicationTest; class ViewControllerTest extends ApplicationTest { - private EventManager testEventManager; - private GenericViewController testViewController; + + /** + * View element instance used for testing. + * */ private GenericViewElement testViewElement; @Override - public void start(Stage stage) { - testEventManager = new EventManager(); - testViewElement = new ViewControllerTest.GenericViewElement(new Pane()); - testViewController = new GenericViewController(testViewElement, testEventManager); + public void start(final Stage stage) { + EventManager testEventManager = new EventManager(); + testViewElement = new GenericViewElement(new Pane()); + new GenericViewController(testViewElement, testEventManager); } @Test void controllerElementSetsButtonBehavior() { - assertFalse(testViewElement.buttonPressed); + assertFalse(testViewElement.getButtonPressed()); testViewElement.getInteractableButton().fire(); - assertTrue(testViewElement.buttonPressed); + assertTrue(testViewElement.getButtonPressed()); } private enum GenericViewActions { + /** + * Action used for testing purposes. + * */ TEST_ACTION; } - private class GenericViewElement extends ViewElement { - public Boolean buttonPressed = false; + /** + * Test class meant for simulating a view element instance. + * + * @see ViewElement + * */ + private static class GenericViewElement extends ViewElement { + private boolean buttonPressed = false; private Button interactableButton; protected GenericViewElement(final Pane rootPane) { @@ -51,11 +61,22 @@ public Button getInteractableButton() { return interactableButton; } + public boolean getButtonPressed() { + return buttonPressed; + } + @Override - protected void initStyling() { } + protected void initStyling() { + // Empty + } } - private class GenericViewController extends ViewController { + /** + * View controller class used for testing. + * + * @see ViewController + * */ + private static class GenericViewController extends ViewController { protected GenericViewController(final ViewControllerTest.GenericViewElement viewElement, final EventManager eventManager) From 757b17b184917ed26df5e8795a7acff23e68f402 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:46 +0200 Subject: [PATCH 23/31] Create ViewElementTest.java --- .../g40/mappe/view/ViewElementTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElementTest.java diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElementTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElementTest.java new file mode 100644 index 0000000..ad4b19e --- /dev/null +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElementTest.java @@ -0,0 +1,107 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import javafx.scene.control.Button; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import org.junit.jupiter.api.Test; +import org.testfx.framework.junit5.ApplicationTest; + +class ViewElementTest extends ApplicationTest { + + /** + * View element instance used for testing. + * */ + private ViewElementTest.GenericViewElement testViewElement; + + /** + * Root of generic view instance. + * */ + private Pane rootPane; + + @Override + public void start(final Stage stage) { + rootPane = new Pane(); + testViewElement = new ViewElementTest.GenericViewElement(rootPane); + } + + @Test + void constructorSetsValuesAsExpected() { + assertEquals(rootPane, testViewElement.getRootPane()); + } + + @Test + void constructorThrowsExceptionWhenIllegalArguments() { + assertDoesNotThrow( + () -> new ViewElementTest.GenericViewElement(new VBox()) + ); + + assertThrows(IllegalArgumentException.class, + () -> new ViewElementTest.GenericViewElement(null) + ); + } + + @Test + void setOnActionThrowsExceptionOnIllegalArguments() { + assertThrows(IllegalArgumentException.class, + () -> testViewElement.setOnAction( + GenericViewActions.UNUSED_TEST_ACTION, + () -> testViewElement.setButtonPressed() + ) + ); + } + + private enum GenericViewActions { + /** + * Action used for testing purposes. + * */ + TEST_ACTION, + + /** + * Unused test action to check exception throwing. + * */ + UNUSED_TEST_ACTION + } + + /** + * Test class meant for simulating a view element instance. + * + * @see ViewElement + * */ + private static class GenericViewElement extends ViewElement { + private boolean buttonPressed = false; + private Button interactableButton; + + protected GenericViewElement(final Pane rootPane) { + super(rootPane, ViewElementTest.GenericViewActions.class); + } + + @Override + protected void initLayout() { + interactableButton = new Button("Click me!"); + registerButton(ViewElementTest.GenericViewActions.TEST_ACTION, interactableButton); + } + + public Button getInteractableButton() { + return interactableButton; + } + + public boolean getButtonPressed() { + return buttonPressed; + } + + public void setButtonPressed() { + buttonPressed = true; + } + + @Override + protected void initStyling() { + // Empty + } + } +} \ No newline at end of file From 006979aa457e1d14862882b4e390539b3bba4566 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:50:48 +0200 Subject: [PATCH 24/31] Update ViewManagerTest.java --- .../g40/mappe/view/ViewManagerTest.java | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManagerTest.java index f4d04e9..0ecc2a9 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManagerTest.java @@ -2,7 +2,6 @@ import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import javafx.scene.Scene; -import javafx.scene.control.Button; import javafx.scene.layout.Pane; import javafx.stage.Stage; import org.junit.jupiter.api.Assertions; @@ -17,16 +16,22 @@ */ class ViewManagerTest extends ApplicationTest { + /** + * View manager object used for testing. + * */ private ViewManager testViewManager; - private EventManager testEventManager; - private ViewManagerTest.GenericViewElement genericViewElement; + + /** + * Generic view element instance used for testing. + * */ + private ViewManagerTest.GenericViewElement testViewElement; @Override public void start(final Stage stage) { stage.setScene(new Scene(new Pane())); - testEventManager = new EventManager(); + EventManager testEventManager = new EventManager(); testViewManager = new ViewManager(stage, testEventManager); - genericViewElement = new GenericViewElement(new Pane(), ViewEnum.IN_GAME); + testViewElement = new GenericViewElement(new Pane(), ViewEnum.IN_GAME); } @Test @@ -36,42 +41,59 @@ void testViewManagerHoldsNoViewAtStart() { @Test void addingMultipleOfSameViewThrowsException() { - testViewManager.addView(genericViewElement); + testViewManager.addView(testViewElement); Assertions.assertThrows(IllegalArgumentException.class, () -> { - testViewManager.addView(genericViewElement); + testViewManager.addView(testViewElement); }); } @Test void testViewManagerSettingCorrectView() { - testViewManager.addView(genericViewElement); - testViewManager.setScene(genericViewElement); - Assertions.assertEquals(genericViewElement, testViewManager.getCurrentView()); + testViewManager.addView(testViewElement); + testViewManager.setScene(testViewElement); + Assertions.assertEquals(testViewElement, testViewManager.getCurrentView()); } @Test void testViewManagerThrowingErrorWhenSettingNonExistentView() { Assertions.assertThrows(IllegalArgumentException.class, () -> { - testViewManager.setScene(genericViewElement); + testViewManager.setScene(testViewElement); }); } @Test void throwsErrorWhenAddingWidgetWithNoViewName() { - genericViewElement = new GenericViewElement(new Pane()); + testViewElement = new GenericViewElement(new Pane()); Assertions.assertThrows(IllegalArgumentException.class, () -> { - testViewManager.addView(genericViewElement); + testViewManager.addView(testViewElement); }); } - private enum GenericActions { - ACTION_1; + @Test + void settingViewThroughViewDataWorksAsExpected() { + GenericViewElement testViewElement2 = + new GenericViewElement(new Pane(), ViewEnum.MAIN_MENU); + + testViewManager.addView(testViewElement2); + ViewData viewData = new ViewData(ViewEnum.MAIN_MENU); + + Assertions.assertNotEquals(testViewElement2, + testViewManager.getCurrentView()); + + testViewManager.setScene(viewData); + Assertions.assertEquals(testViewElement2, testViewManager.getCurrentView()); } + private enum GenericActions { + // Empty. + } - private class GenericViewElement extends ViewElement { - public Boolean buttonPressed = false; - private Button interactableButton; + /** + * Generic view element class used to test view elements within + * the view manager. + * */ + private static class GenericViewElement + extends ViewElement { protected GenericViewElement(final Pane rootPane, final ViewEnum viewName) { super(rootPane, viewName, GenericActions.class); @@ -83,15 +105,12 @@ protected GenericViewElement(final Pane rootPane) { @Override protected void initLayout() { - interactableButton = new Button("Click me!"); - registerButton(GenericActions.ACTION_1, interactableButton); - } - - public Button getInteractableButton() { - return interactableButton; + // Empty for view manager testing. } @Override - protected void initStyling() { } + protected void initStyling() { + // Empty for view manager testing. + } } } From 5fe81c2be588ceed10f099358299c8f7292ab90d Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:53:06 +0200 Subject: [PATCH 25/31] Feat: Renamed File Parser to File Manager, and File Converter to File Parser --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 6 +- .../g40/mappe/service/FileConverter.java | 93 -------- .../g40/mappe/service/FileManager.java | 187 ++++++++++++++++ .../g40/mappe/service/FileParser.java | 204 +++++------------- .../g40/mappe/service/package-info.java | 2 +- .../g40/mappe/service/FileConverterTest.java | 72 ------- .../g40/mappe/service/FileManagerTest.java | 60 ++++++ .../g40/mappe/service/FileParserTest.java | 82 ++++--- 8 files changed, 353 insertions(+), 353 deletions(-) delete mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverter.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java delete mode 100644 src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverterTest.java create mode 100644 src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index 3caea4b..dd5a61d 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -3,8 +3,8 @@ import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; -import edu.ntnu.idi.idatt2003.g40.mappe.service.FileConverter; import edu.ntnu.idi.idatt2003.g40.mappe.service.FileParser; +import edu.ntnu.idi.idatt2003.g40.mappe.service.FileManager; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues; @@ -86,9 +86,9 @@ public void start(final Stage stage) throws Exception { ViewManager viewManager = new ViewManager(stage, eventManager); List stocksInFile; - FileParser parser1 = new FileParser("/sp500.csv"); + FileManager parser1 = new FileManager("/sp500.csv"); - FileConverter converter1 = new FileConverter(); + FileParser converter1 = new FileParser(); stocksInFile = converter1.getStocksFromStrings(parser1.readFile()); Exchange exchange = new Exchange("Exchange", stocksInFile); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverter.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverter.java deleted file mode 100644 index 397328d..0000000 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverter.java +++ /dev/null @@ -1,93 +0,0 @@ -package edu.ntnu.idi.idatt2003.g40.mappe.service; - -import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -/** - * Converts stock objects to/from string format for file handling. - * - *

    Responsibilities:

    - *
      - *
    • Turn a valid list of stock string elements to a - * list of {@link Stock} objects.
    • - * - *
    • Turn a list of stock objects to a - * list of string elements.
    • - *
    - * - *

    Used with {@link FileParser}

    - * - * @see FileParser - * @author tohja - * @version 1.0.0 - * */ -public class FileConverter { - - /** - * Turns a list of valid string representations - * of stock objects to a list of stock objects. - * - * @param validStocks list of string elements properly - * representing stock objects. - * - * @return {@link List} - * - * @throws IllegalArgumentException if stock object(s) cannot be converted, - * or if list is null/empty. - * - * */ - public List getStocksFromStrings(final List validStocks) - throws IllegalArgumentException { - if (validStocks == null || validStocks.isEmpty()) { - throw new IllegalArgumentException("Empty or null stock list!"); - } else { - List stocksFromFile = new ArrayList<>(); - List stockSymbols = new ArrayList<>(); - - validStocks.forEach(s -> { - String[] lineElements = s.split(","); - String stockSymbol = lineElements[0].trim(); - String stockName = lineElements[1].trim(); - BigDecimal stockPrice = new BigDecimal(lineElements[2].trim()); - - try { - Stock stockObject = new Stock(stockSymbol, stockName, stockPrice); - if (!stockSymbols.contains(stockSymbol)) { - stockSymbols.add(stockSymbol); - stocksFromFile.add(stockObject); - } - } catch (IllegalArgumentException e) { - System.err.println("(" + s + ") is not a valid stock! Skipping..."); - } - - }); - return stocksFromFile; - } - } - - /** - * Converts a list of stocks to string representations of that stock. - * - *

    format: SYMBOL, NAME, PRICE

    - * - * @param stocks a list of {@link Stock} objects. - * - * @return a list of string representation of the stock objects. - * - * @throws IllegalArgumentException if stocks is empty or null. - * */ - public List stocksToStrings(final List stocks) { - if (stocks == null || stocks.isEmpty()) { - throw new IllegalArgumentException("Empty or null stock list!"); - } else { - ArrayList stringList = new ArrayList<>(); - stocks.forEach(s -> - stringList.add(s.getSymbol().trim() + "," + s.getCompany().trim() - + "," + s.getSalesPrice().toString()) - ); - return stringList; - } - } -} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java new file mode 100644 index 0000000..cd674cd --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java @@ -0,0 +1,187 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.service; + +import java.io.*; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.function.Predicate; + +/** + * Class used to parse (filter) valid stocks from a .txt file. + * + *

    Responsibilities:

    + *
      + *
    • Read file and return a filtered list of string elements, + * where each element represents a valid stock line.
    • + * + *
    • Write a list of string representations of stock objects + * to file, each stock separated by a line.
    • + *
    + * + *

    Used with {@link FileParser}

    + * + * @see FileParser + * @author tohja + * @version 1.0.0 + * */ +public class FileManager { + + /** The path name this parser is using.*/ + private final String pathName; + + /** + * Rule set for validating lines and strings. + * + *

    Uses predicates.

    + * */ + private enum ParserRuleSet { + + /** + * Rule for whether string is empty or not. + * */ + NOT_EMPTY(s -> !s.trim().isEmpty()), + + /** + * Rule for if string is comment or not. + * */ + NOT_COMMENT(s -> !s.startsWith("#")), + + /** + * Rule for if line is in valid format. + * */ + VALID_FORMAT(NOT_EMPTY.rule.and(NOT_COMMENT.rule)), + + /** + * Rule for if string is considered a valid company symbol. + * */ + VALID_CODE(s -> s.matches("[A-Z]{4}")), + + /** + * Rule for if string is a valid company name. + * */ + VALID_NAME(s -> s.matches(".*")), + + /** + * Rule for if string can be parsed to a {@link BigDecimal} object. + * */ + CAN_PARSE_TO_BIG_DECIMAL(s -> { + try { + new BigDecimal(s); + return true; + } catch (NumberFormatException e) { + return false; + } + }), + + /** + * Rule for if string is in a valid price format. + * */ + VALID_PRICE_FORMAT(s -> s.matches("[^a-zA-Z]+")), + + /** + * Rule for if string is a valid price. + * */ + VALID_PRICE(VALID_PRICE_FORMAT.rule.and(CAN_PARSE_TO_BIG_DECIMAL.rule)); + + /** + * The constants' rules as predicates with input of type string. + * */ + private final Predicate rule; + + ParserRuleSet(final Predicate rule) { + this.rule = rule; + } + } + + /** + * Constructor. + * + * @param pathName the file path name to read. + * */ + public FileManager(final String pathName) { + this.pathName = pathName; + } + + /** + * Reads the file and returns a list element of all valid stocks as strings. + * + *

    Uses {@link BufferedReader} for opening a file stream.

    + * + * @return {@link List} object of all valid stock strings in file. + * + * @throws IOException if path cannot be read. + * + * @see Path + * */ + + public List readFile() throws IOException { + try (InputStream inputStream = getClass().getResourceAsStream(pathName); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + + List allLines = bufferedReader.readAllLines(); + List readableLines = + allLines.stream() + .filter(ParserRuleSet.VALID_FORMAT.rule).toList(); + + // Valid lines (following the correct regular expressions) + return readableLines.stream().filter(s -> { + String[] parts = s.trim().split(","); + + if (parts.length != 3) { + return false; + } + + boolean validCode = ParserRuleSet + .VALID_CODE.rule.test(parts[0].trim()); + + boolean validName = ParserRuleSet + .VALID_NAME.rule.test(parts[1].trim()); + + boolean validPrice = ParserRuleSet + .VALID_PRICE.rule.test(parts[2].trim()); + + return validCode && validName && validPrice; + }).toList(); + + } catch (IOException e) { + throw new IOException("File parser could not parse file!"); + } + } + + /** + * Writes a given lists of stocks to the file. + * + *

    Uses {@link BufferedWriter}.

    + * + * @param stringList list of strings representing stocks in the format + * SYMBOL, NAME, PRICE + * + * @throws IOException if writing process throws error. + * */ + public void writeStocksToFile(final List stringList) + throws IllegalArgumentException, IOException { + if (stringList == null || stringList.isEmpty()) { + throw new IllegalArgumentException("Empty or null list of stocks!"); + } else { + Path path = Paths.get(pathName); + + try (BufferedWriter writer = Files.newBufferedWriter(path, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND)) { + + writer.newLine(); + + for (String line : stringList) { + writer.write(line); + writer.newLine(); + } + } catch (IOException e) { + throw new IOException("Error during buffered write", e); + } + } + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java index 481d423..c621a47 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java @@ -1,187 +1,93 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; -import java.io.*; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.List; -import java.util.function.Predicate; /** - * Class used to parse (filter) valid stocks from a .txt file. + * Converts stock objects to/from string format for file handling. * *

    Responsibilities:

    *
      - *
    • Read file and return a filtered list of string elements, - * where each element represents a valid stock line.
    • + *
    • Turn a valid list of stock string elements to a + * list of {@link Stock} objects.
    • * - *
    • Write a list of string representations of stock objects - * to file, each stock separated by a line.
    • + *
    • Turn a list of stock objects to a + * list of string elements.
    • *
    * - *

    Used with {@link FileConverter}

    + *

    Used with {@link FileManager}

    * - * @see FileConverter + * @see FileManager * @author tohja * @version 1.0.0 * */ public class FileParser { - /** The path name this parser is using.*/ - private final String pathName; - - /** - * Rule set for validating lines and strings. - * - *

    Uses predicates.

    - * */ - private enum ParserRuleSet { - - /** - * Rule for whether string is empty or not. - * */ - NOT_EMPTY(s -> !s.trim().isEmpty()), - - /** - * Rule for if string is comment or not. - * */ - NOT_COMMENT(s -> !s.startsWith("#")), - - /** - * Rule for if line is in valid format. - * */ - VALID_FORMAT(NOT_EMPTY.rule.and(NOT_COMMENT.rule)), - - /** - * Rule for if string is considered a valid company symbol. - * */ - VALID_CODE(s -> s.matches("[A-Z]{4}")), - - /** - * Rule for if string is a valid company name. - * */ - VALID_NAME(s -> s.matches(".*")), - - /** - * Rule for if string can be parsed to a {@link BigDecimal} object. - * */ - CAN_PARSE_TO_BIG_DECIMAL(s -> { - try { - new BigDecimal(s); - return true; - } catch (NumberFormatException e) { - return false; - } - }), - - /** - * Rule for if string is in a valid price format. - * */ - VALID_PRICE_FORMAT(s -> s.matches("[^a-zA-Z]+")), - - /** - * Rule for if string is a valid price. - * */ - VALID_PRICE(VALID_PRICE_FORMAT.rule.and(CAN_PARSE_TO_BIG_DECIMAL.rule)); - - /** - * The constants' rules as predicates with input of type string. - * */ - private final Predicate rule; - - ParserRuleSet(final Predicate rule) { - this.rule = rule; - } - } - - /** - * Constructor. - * - * @param pathName the file path name to read. - * */ - public FileParser(final String pathName) { - this.pathName = pathName; - } - /** - * Reads the file and returns a list element of all valid stocks as strings. + * Turns a list of valid string representations + * of stock objects to a list of stock objects. * - *

    Uses {@link BufferedReader} for opening a file stream.

    + * @param validStocks list of string elements properly + * representing stock objects. * - * @return {@link List} object of all valid stock strings in file. + * @return {@link List} * - * @throws IOException if path cannot be read. + * @throws IllegalArgumentException if stock object(s) cannot be converted, + * or if list is null/empty. * - * @see Path * */ - - public List readFile() throws IOException { - try (InputStream inputStream = getClass().getResourceAsStream(pathName); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - - List allLines = bufferedReader.readAllLines(); - List readableLines = - allLines.stream() - .filter(ParserRuleSet.VALID_FORMAT.rule).toList(); - - // Valid lines (following the correct regular expressions) - return readableLines.stream().filter(s -> { - String[] parts = s.trim().split(","); - - if (parts.length != 3) { - return false; + public List getStocksFromStrings(final List validStocks) + throws IllegalArgumentException { + if (validStocks == null || validStocks.isEmpty()) { + throw new IllegalArgumentException("Empty or null stock list!"); + } else { + List stocksFromFile = new ArrayList<>(); + List stockSymbols = new ArrayList<>(); + + validStocks.forEach(s -> { + String[] lineElements = s.split(","); + String stockSymbol = lineElements[0].trim(); + String stockName = lineElements[1].trim(); + BigDecimal stockPrice = new BigDecimal(lineElements[2].trim()); + + try { + Stock stockObject = new Stock(stockSymbol, stockName, stockPrice); + if (!stockSymbols.contains(stockSymbol)) { + stockSymbols.add(stockSymbol); + stocksFromFile.add(stockObject); + } + } catch (IllegalArgumentException e) { + System.err.println("(" + s + ") is not a valid stock! Skipping..."); } - boolean validCode = ParserRuleSet - .VALID_CODE.rule.test(parts[0].trim()); - - boolean validName = ParserRuleSet - .VALID_NAME.rule.test(parts[1].trim()); - - boolean validPrice = ParserRuleSet - .VALID_PRICE.rule.test(parts[2].trim()); - - return validCode && validName && validPrice; - }).toList(); - - } catch (IOException e) { - throw new IOException("File parser could not parse file!"); + }); + return stocksFromFile; } } /** - * Writes a given lists of stocks to the file. + * Converts a list of stocks to string representations of that stock. + * + *

    format: SYMBOL, NAME, PRICE

    * - *

    Uses {@link BufferedWriter}.

    + * @param stocks a list of {@link Stock} objects. * - * @param stringList list of strings representing stocks in the format - * SYMBOL, NAME, PRICE + * @return a list of string representation of the stock objects. * - * @throws IOException if writing process throws error. + * @throws IllegalArgumentException if stocks is empty or null. * */ - public void writeStocksToFile(final List stringList) - throws IllegalArgumentException, IOException { - if (stringList == null || stringList.isEmpty()) { - throw new IllegalArgumentException("Empty or null list of stocks!"); + public List stocksToStrings(final List stocks) { + if (stocks == null || stocks.isEmpty()) { + throw new IllegalArgumentException("Empty or null stock list!"); } else { - Path path = Paths.get(pathName); - - try (BufferedWriter writer = Files.newBufferedWriter(path, - StandardOpenOption.CREATE, - StandardOpenOption.APPEND)) { - - writer.newLine(); - - for (String line : stringList) { - writer.write(line); - writer.newLine(); - } - } catch (IOException e) { - throw new IOException("Error during buffered write", e); - } + ArrayList stringList = new ArrayList<>(); + stocks.forEach(s -> + stringList.add(s.getSymbol().trim() + "," + s.getCompany().trim() + + "," + s.getSalesPrice().toString()) + ); + return stringList; } } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java index 633c60a..9a013e4 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java @@ -1,5 +1,5 @@ /** * Contains classes providing modular functionality to the application, - * such as the {@link edu.ntnu.idi.idatt2003.g40.mappe.service.FileConverter}. + * such as the {@link edu.ntnu.idi.idatt2003.g40.mappe.service.FileParser}. * */ package edu.ntnu.idi.idatt2003.g40.mappe.service; diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverterTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverterTest.java deleted file mode 100644 index 5cb5444..0000000 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileConverterTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package edu.ntnu.idi.idatt2003.g40.mappe.service; - -import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -class FileConverterTest { - - private FileConverter converter; - - private String validStockAsString1; - private String validStockAsString2; - private String validStockAsString3; - private ArrayList allStocks; - private String invalidStockAsString1; - - @BeforeEach - void setUp() { - validStockAsString1 = "AAPL, Apple inc., 251.42"; - validStockAsString2 = "NVID, Nvidia corp., 100.25"; - validStockAsString3 = "SAMS, Samsung corporation, 103.21"; - - invalidStockAsString1 = "INVALID, This stock has an invalid code!, 100.21"; - - allStocks = new ArrayList<>(); - - allStocks.add(validStockAsString1); - allStocks.add(validStockAsString2); - allStocks.add(validStockAsString3); - allStocks.add(invalidStockAsString1); - - converter = new FileConverter(); - } - - @Test - void converter_returns_valid_stock_apple() { - - boolean stockIncluded = false; - - List stocksFromConverter = converter.getStocksFromStrings(allStocks); - - for (Stock s : stocksFromConverter) { - if (s.getSymbol().equals("AAPL")) { - stockIncluded = true; - break; - } - } - - Assertions.assertTrue(stockIncluded); - } - - @Test - void converter_ignores_invalid_stock_representation() { - - boolean stockIncluded = false; - - List stocksFromConverter = converter.getStocksFromStrings(allStocks); - - for (Stock s : stocksFromConverter) { - if (s.getSymbol().equals("INVALID")) { - stockIncluded = true; - break; - } - } - - Assertions.assertFalse(stockIncluded); - } -} \ No newline at end of file diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java new file mode 100644 index 0000000..49dbd7e --- /dev/null +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java @@ -0,0 +1,60 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FileManagerTest { + + private final String testStockDataPath = "/testStockData.txt"; + + private final String absoluteTestStockDataPath = "src/main/resources/testStockData.txt"; + FileManager fileManager; + + private final String validStockFromFile = "NVID, Nvidida Corporation, 241.591"; + + private final String invalidStockFromFile = "COOLI, This is a cool name, 252.2"; + + private final String commentFromFile = "#Above me are some valid formats."; + + private List allLines = new ArrayList<>(); + + private List validStocks = new ArrayList<>(); + + @BeforeEach + void setUp() throws Exception { + fileManager = new FileManager(testStockDataPath); + Path path = Paths.get(absoluteTestStockDataPath); + allLines = Files.readAllLines(path); + try { + validStocks = fileManager.readFile(); + } catch (Exception _) { + throw new Exception("Test failed"); + } + } + + @Test + void parser_gets_valid_stock_from_file() { + assertTrue(allLines.contains(validStockFromFile)); + assertTrue(validStocks.contains(validStockFromFile)); + } + + @Test + void parser_skips_comments_from_file() { + assertTrue(allLines.contains(commentFromFile)); + assertFalse(validStocks.contains(commentFromFile)); + } + + @Test + void parser_skips_invalid_stock_from_file() { + assertTrue(allLines.contains(invalidStockFromFile)); + assertFalse(validStocks.contains(invalidStockFromFile)); + } +} \ No newline at end of file diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java index 7d87b1a..fb59fc6 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java @@ -1,60 +1,72 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; - class FileParserTest { - private final String testStockDataPath = "/testStockData.txt"; - - private final String absoluteTestStockDataPath = "src/main/resources/testStockData.txt"; - FileParser fileParser; + private FileParser converter; - private final String validStockFromFile = "NVID, Nvidida Corporation, 241.591"; + private String validStockAsString1; + private String validStockAsString2; + private String validStockAsString3; + private ArrayList allStocks; + private String invalidStockAsString1; - private final String invalidStockFromFile = "COOLI, This is a cool name, 252.2"; + @BeforeEach + void setUp() { + validStockAsString1 = "AAPL, Apple inc., 251.42"; + validStockAsString2 = "NVID, Nvidia corp., 100.25"; + validStockAsString3 = "SAMS, Samsung corporation, 103.21"; - private final String commentFromFile = "#Above me are some valid formats."; + invalidStockAsString1 = "INVALID, This stock has an invalid code!, 100.21"; - private List allLines = new ArrayList<>(); + allStocks = new ArrayList<>(); - private List validStocks = new ArrayList<>(); + allStocks.add(validStockAsString1); + allStocks.add(validStockAsString2); + allStocks.add(validStockAsString3); + allStocks.add(invalidStockAsString1); - @BeforeEach - void setUp() throws Exception { - fileParser = new FileParser(testStockDataPath); - Path path = Paths.get(absoluteTestStockDataPath); - allLines = Files.readAllLines(path); - try { - validStocks = fileParser.readFile(); - } catch (Exception _) { - throw new Exception("Test failed"); - } + converter = new FileParser(); } @Test - void parser_gets_valid_stock_from_file() { - assertTrue(allLines.contains(validStockFromFile)); - assertTrue(validStocks.contains(validStockFromFile)); - } + void converter_returns_valid_stock_apple() { - @Test - void parser_skips_comments_from_file() { - assertTrue(allLines.contains(commentFromFile)); - assertFalse(validStocks.contains(commentFromFile)); + boolean stockIncluded = false; + + List stocksFromConverter = converter.getStocksFromStrings(allStocks); + + for (Stock s : stocksFromConverter) { + if (s.getSymbol().equals("AAPL")) { + stockIncluded = true; + break; + } + } + + Assertions.assertTrue(stockIncluded); } @Test - void parser_skips_invalid_stock_from_file() { - assertTrue(allLines.contains(invalidStockFromFile)); - assertFalse(validStocks.contains(invalidStockFromFile)); + void converter_ignores_invalid_stock_representation() { + + boolean stockIncluded = false; + + List stocksFromConverter = converter.getStocksFromStrings(allStocks); + + for (Stock s : stocksFromConverter) { + if (s.getSymbol().equals("INVALID")) { + stockIncluded = true; + break; + } + } + + Assertions.assertFalse(stockIncluded); } } \ No newline at end of file From 9dca3038d3a8c5b75f7a0fa6fbbc85cfa4d2c7bf Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 19:55:16 +0200 Subject: [PATCH 26/31] Feat: Renamed FileParser and FileManager to StockFileParser and StockFileManager, respectively. --- .../java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java | 8 ++++---- .../idatt2003/g40/mappe/service/SaveGameService.java | 1 - .../{FileManager.java => StockFileManager.java} | 8 ++++---- .../{FileParser.java => StockFileParser.java} | 6 +++--- .../g40/mappe/service/TransactionFactory.java | 1 - .../idatt2003/g40/mappe/service/package-info.java | 2 +- ...ileManagerTest.java => StockFileManagerTest.java} | 8 ++++---- ...{FileParserTest.java => StockFileParserTest.java} | 6 +++--- .../g40/mappe/service/event/EventManagerTest.java | 12 ++++++++++++ 9 files changed, 31 insertions(+), 21 deletions(-) rename src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/{FileManager.java => StockFileManager.java} (97%) rename src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/{FileParser.java => StockFileParser.java} (96%) rename src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/{FileManagerTest.java => StockFileManagerTest.java} (89%) rename src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/{FileParserTest.java => StockFileParserTest.java} (94%) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index dd5a61d..0ef6962 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -3,8 +3,8 @@ import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; -import edu.ntnu.idi.idatt2003.g40.mappe.service.FileParser; -import edu.ntnu.idi.idatt2003.g40.mappe.service.FileManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.StockFileParser; +import edu.ntnu.idi.idatt2003.g40.mappe.service.StockFileManager; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues; @@ -86,9 +86,9 @@ public void start(final Stage stage) throws Exception { ViewManager viewManager = new ViewManager(stage, eventManager); List stocksInFile; - FileManager parser1 = new FileManager("/sp500.csv"); + StockFileManager parser1 = new StockFileManager("/sp500.csv"); - FileParser converter1 = new FileParser(); + StockFileParser converter1 = new StockFileParser(); stocksInFile = converter1.getStocksFromStrings(parser1.readFile()); Exchange exchange = new Exchange("Exchange", stocksInFile); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java index 2c6aaed..0350071 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java @@ -1,7 +1,6 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; - import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java similarity index 97% rename from src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java rename to src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java index cd674cd..b1a782d 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java @@ -22,13 +22,13 @@ * to file, each stock separated by a line. * * - *

    Used with {@link FileParser}

    + *

    Used with {@link StockFileParser}

    * - * @see FileParser + * @see StockFileParser * @author tohja * @version 1.0.0 * */ -public class FileManager { +public class StockFileManager { /** The path name this parser is using.*/ private final String pathName; @@ -102,7 +102,7 @@ private enum ParserRuleSet { * * @param pathName the file path name to read. * */ - public FileManager(final String pathName) { + public StockFileManager(final String pathName) { this.pathName = pathName; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java similarity index 96% rename from src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java rename to src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java index c621a47..b08558d 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParser.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java @@ -17,13 +17,13 @@ * list of string elements. * * - *

    Used with {@link FileManager}

    + *

    Used with {@link StockFileManager}

    * - * @see FileManager + * @see StockFileManager * @author tohja * @version 1.0.0 * */ -public class FileParser { +public class StockFileParser { /** * Turns a list of valid string representations diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java index 490a93c..62e849c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/TransactionFactory.java @@ -23,7 +23,6 @@ private TransactionFactory() { * @param transactionType the type of transaction to create. * @param share the share this transaction is about. * @param week the week this transaction takes place in. - * @param calculator the calculator to use when calculating the transaction. * * @return an implementation of {@link Transaction}. * */ diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java index 9a013e4..10593f1 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/package-info.java @@ -1,5 +1,5 @@ /** * Contains classes providing modular functionality to the application, - * such as the {@link edu.ntnu.idi.idatt2003.g40.mappe.service.FileParser}. + * such as the {@link edu.ntnu.idi.idatt2003.g40.mappe.service.StockFileParser}. * */ package edu.ntnu.idi.idatt2003.g40.mappe.service; diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java similarity index 89% rename from src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java rename to src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java index 49dbd7e..9706b5c 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java @@ -11,12 +11,12 @@ import static org.junit.jupiter.api.Assertions.*; -class FileManagerTest { +class StockFileManagerTest { private final String testStockDataPath = "/testStockData.txt"; private final String absoluteTestStockDataPath = "src/main/resources/testStockData.txt"; - FileManager fileManager; + StockFileManager stockFileManager; private final String validStockFromFile = "NVID, Nvidida Corporation, 241.591"; @@ -30,11 +30,11 @@ class FileManagerTest { @BeforeEach void setUp() throws Exception { - fileManager = new FileManager(testStockDataPath); + stockFileManager = new StockFileManager(testStockDataPath); Path path = Paths.get(absoluteTestStockDataPath); allLines = Files.readAllLines(path); try { - validStocks = fileManager.readFile(); + validStocks = stockFileManager.readFile(); } catch (Exception _) { throw new Exception("Test failed"); } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java similarity index 94% rename from src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java rename to src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java index fb59fc6..61ecfb2 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/FileParserTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java @@ -8,9 +8,9 @@ import java.util.ArrayList; import java.util.List; -class FileParserTest { +class StockFileParserTest { - private FileParser converter; + private StockFileParser converter; private String validStockAsString1; private String validStockAsString2; @@ -33,7 +33,7 @@ void setUp() { allStocks.add(validStockAsString3); allStocks.add(invalidStockAsString1); - converter = new FileParser(); + converter = new StockFileParser(); } @Test diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java index 4852d1c..eee20c2 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventManagerTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,8 +29,19 @@ private enum TestEventTypes implements EventChannel { } + /** + * Event manager used for testing. + * */ private EventManager testEventManager; + + /** + * Example event subscriber 1. + * */ private SampleEventSubscriber sampleEventSubscriber1; + + /** + * Example event subscriber 2. + * */ private SampleEventSubscriber sampleEventSubscriber2; @BeforeEach From 329c2f1b595f83f954f5be64cfc8ebbcec748fb8 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 20:24:58 +0200 Subject: [PATCH 27/31] Feat: Updated File Manager to have a fallback resource Also updated tests to be independent on external files. --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 2 +- .../g40/mappe/service/StockFileManager.java | 73 ++++++++++++---- src/main/resources/testStockData.txt | 11 --- .../mappe/service/StockFileManagerTest.java | 86 ++++++++++--------- 4 files changed, 101 insertions(+), 71 deletions(-) delete mode 100644 src/main/resources/testStockData.txt diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index 0ef6962..10ae9b9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -86,7 +86,7 @@ public void start(final Stage stage) throws Exception { ViewManager viewManager = new ViewManager(stage, eventManager); List stocksInFile; - StockFileManager parser1 = new StockFileManager("/sp500.csv"); + StockFileManager parser1 = new StockFileManager("src/main/resources/sp500.csv"); StockFileParser converter1 = new StockFileParser(); stocksInFile = converter1.getStocksFromStrings(parser1.readFile()); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java index b1a782d..ac5cb7c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManager.java @@ -1,6 +1,10 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -72,7 +76,7 @@ private enum ParserRuleSet { try { new BigDecimal(s); return true; - } catch (NumberFormatException e) { + } catch (NumberFormatException _) { return false; } }), @@ -109,6 +113,9 @@ public StockFileManager(final String pathName) { /** * Reads the file and returns a list element of all valid stocks as strings. * + *

    If file is not found, + * falls back to default file in resources folder.

    + * *

    Uses {@link BufferedReader} for opening a file stream.

    * * @return {@link List} object of all valid stock strings in file. @@ -117,17 +124,20 @@ public StockFileManager(final String pathName) { * * @see Path * */ - public List readFile() throws IOException { - try (InputStream inputStream = getClass().getResourceAsStream(pathName); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + Path path = Paths.get(pathName); + + if (!Files.exists(path)) { + extractResourceFallback(path); + } - List allLines = bufferedReader.readAllLines(); - List readableLines = - allLines.stream() - .filter(ParserRuleSet.VALID_FORMAT.rule).toList(); + try (BufferedReader bufferedReader = + Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + List allLines = bufferedReader.lines().toList(); + List readableLines = allLines.stream() + .filter(ParserRuleSet.VALID_FORMAT.rule) + .toList(); - // Valid lines (following the correct regular expressions) return readableLines.stream().filter(s -> { String[] parts = s.trim().split(","); @@ -135,20 +145,45 @@ public List readFile() throws IOException { return false; } - boolean validCode = ParserRuleSet - .VALID_CODE.rule.test(parts[0].trim()); - - boolean validName = ParserRuleSet - .VALID_NAME.rule.test(parts[1].trim()); - - boolean validPrice = ParserRuleSet - .VALID_PRICE.rule.test(parts[2].trim()); + boolean validCode = + ParserRuleSet.VALID_CODE.rule.test(parts[0].trim()); + boolean validName = + ParserRuleSet.VALID_NAME.rule.test(parts[1].trim()); + boolean validPrice = + ParserRuleSet.VALID_PRICE.rule.test(parts[2].trim()); return validCode && validName && validPrice; }).toList(); } catch (IOException e) { - throw new IOException("File parser could not parse file!"); + throw new IOException("File parser could not parse file!", e); + } + } + + /** + * Extracts the fallback template file from the application resources to + * the local file system. + * + * @param targetPath path to send the fallback file. + * + * @throws IOException if resource or input stream is not found or null. + */ + private void extractResourceFallback(final Path targetPath) + throws IOException { + String resourceName = "/sp500.csv"; + + try (InputStream inputStream = getClass().getResourceAsStream(resourceName)) { + if (inputStream == null) { + throw new FileNotFoundException( + "Resource file not found in JAR: " + resourceName + ); + } + + if (targetPath.getParent() != null) { + Files.createDirectories(targetPath.getParent()); + } + + Files.copy(inputStream, targetPath); } } diff --git a/src/main/resources/testStockData.txt b/src/main/resources/testStockData.txt deleted file mode 100644 index b349d0e..0000000 --- a/src/main/resources/testStockData.txt +++ /dev/null @@ -1,11 +0,0 @@ -#THIS IS A COMMENT. - -AAPL, Apple Inc., 276.43 -NVID, Nvidida Corporation, 241.591 - -#Above me are some valid formats. -#Below me are some invalid formats - -COOLI, This is a cool name, 252.2 - -COOL, This is a cool name, 252.2a \ No newline at end of file diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java index 9706b5c..6630d44 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java @@ -1,60 +1,66 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class StockFileManagerTest { - private final String testStockDataPath = "/testStockData.txt"; - - private final String absoluteTestStockDataPath = "src/main/resources/testStockData.txt"; - StockFileManager stockFileManager; - - private final String validStockFromFile = "NVID, Nvidida Corporation, 241.591"; - - private final String invalidStockFromFile = "COOLI, This is a cool name, 252.2"; + /** + * Path used to test file system. + * */ + @TempDir + private Path tempDir; - private final String commentFromFile = "#Above me are some valid formats."; - - private List allLines = new ArrayList<>(); + @Test + void readFileParsesValidStocksAndFiltersOutCommentsAndInvalidData() + throws IOException { + Path tempFile = tempDir.resolve("testStockData.txt"); + List mockFileData = List.of( + "# This is a comment header line and should be skipped", + "NVID, Nvidida Corporation, 241.591", + "AAPL, Apple Inc, 175.50", + "", + "INVALID_ROW_MISSING_COLUMNS", + "COOLI, This is a cool name but missing price token" + ); + Files.write(tempFile, mockFileData); - private List validStocks = new ArrayList<>(); + StockFileManager stockFileManager = + new StockFileManager(tempFile.toString()); + List parsingResults = stockFileManager.readFile(); - @BeforeEach - void setUp() throws Exception { - stockFileManager = new StockFileManager(testStockDataPath); - Path path = Paths.get(absoluteTestStockDataPath); - allLines = Files.readAllLines(path); - try { - validStocks = stockFileManager.readFile(); - } catch (Exception _) { - throw new Exception("Test failed"); - } + assertEquals(2, parsingResults.size()); + assertTrue(parsingResults.contains("NVID, Nvidida Corporation, 241.591")); + assertTrue(parsingResults.contains("AAPL, Apple Inc, 175.50")); + assertFalse(parsingResults.stream().anyMatch(line -> line.startsWith("#"))); } @Test - void parser_gets_valid_stock_from_file() { - assertTrue(allLines.contains(validStockFromFile)); - assertTrue(validStocks.contains(validStockFromFile)); - } + void readFileHandlesEmptyFileGracefully() throws IOException { + Path emptyFile = tempDir.resolve("emptyStockData.txt"); + Files.createFile(emptyFile); - @Test - void parser_skips_comments_from_file() { - assertTrue(allLines.contains(commentFromFile)); - assertFalse(validStocks.contains(commentFromFile)); + StockFileManager stockFileManager = new StockFileManager(emptyFile.toString()); + List results = stockFileManager.readFile(); + + assertTrue(results.isEmpty()); } @Test - void parser_skips_invalid_stock_from_file() { - assertTrue(allLines.contains(invalidStockFromFile)); - assertFalse(validStocks.contains(invalidStockFromFile)); + void constructorOrReadFileDoesNotThrowExceptionOnMissingFile() { + StockFileManager stockFileManager = + new StockFileManager("non_existent_directory/missing_file.txt"); + + assertDoesNotThrow(stockFileManager::readFile); } -} \ No newline at end of file +} From 0bc91d8260faa0ea936cac88d8612cca7c820b94 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 20:25:43 +0200 Subject: [PATCH 28/31] Fix: Fixed bug where test would create stock file --- .../idatt2003/g40/mappe/service/StockFileManagerTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java index 6630d44..81d5f95 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java @@ -55,12 +55,4 @@ void readFileHandlesEmptyFileGracefully() throws IOException { assertTrue(results.isEmpty()); } - - @Test - void constructorOrReadFileDoesNotThrowExceptionOnMissingFile() { - StockFileManager stockFileManager = - new StockFileManager("non_existent_directory/missing_file.txt"); - - assertDoesNotThrow(stockFileManager::readFile); - } } From 93969e08841f955e21bbfb095780772172a66aac Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 20:42:16 +0200 Subject: [PATCH 29/31] Feat: Refactor FileManager and FileParser, unit testing --- .../g40/mappe/service/StockFileParser.java | 60 ++++++++++------- .../mappe/service/StockFileManagerTest.java | 2 - .../mappe/service/StockFileParserTest.java | 65 ++++++++++++++----- 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java index b08558d..07fd802 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParser.java @@ -3,7 +3,9 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Converts stock objects to/from string format for file handling. @@ -42,29 +44,29 @@ public List getStocksFromStrings(final List validStocks) throws IllegalArgumentException { if (validStocks == null || validStocks.isEmpty()) { throw new IllegalArgumentException("Empty or null stock list!"); - } else { - List stocksFromFile = new ArrayList<>(); - List stockSymbols = new ArrayList<>(); + } + List stocksFromFile = new ArrayList<>(); + Set stockSymbols = new HashSet<>(); - validStocks.forEach(s -> { - String[] lineElements = s.split(","); - String stockSymbol = lineElements[0].trim(); - String stockName = lineElements[1].trim(); - BigDecimal stockPrice = new BigDecimal(lineElements[2].trim()); + validStocks.forEach(s -> { + String[] lineElements = s.split(","); + String stockSymbol = lineElements[0].trim(); + String stockName = lineElements[1].trim(); + BigDecimal stockPrice = new BigDecimal(lineElements[2].trim()); - try { - Stock stockObject = new Stock(stockSymbol, stockName, stockPrice); - if (!stockSymbols.contains(stockSymbol)) { - stockSymbols.add(stockSymbol); - stocksFromFile.add(stockObject); - } - } catch (IllegalArgumentException e) { - System.err.println("(" + s + ") is not a valid stock! Skipping..."); + try { + Stock stockObject = new Stock(stockSymbol, stockName, stockPrice); + if (stockSymbols.add(stockSymbol)) { + stocksFromFile.add(stockObject); } - - }); - return stocksFromFile; + } catch (IllegalArgumentException _) { + // Ignore invalid strings. + } + }); + if (stocksFromFile.isEmpty()) { + throw new IllegalArgumentException("No stocks parsed succesfully!"); } + return stocksFromFile; } /** @@ -81,13 +83,21 @@ public List getStocksFromStrings(final List validStocks) public List stocksToStrings(final List stocks) { if (stocks == null || stocks.isEmpty()) { throw new IllegalArgumentException("Empty or null stock list!"); - } else { - ArrayList stringList = new ArrayList<>(); - stocks.forEach(s -> - stringList.add(s.getSymbol().trim() + "," + s.getCompany().trim() - + "," + s.getSalesPrice().toString()) + } + + List stringList = new ArrayList<>(); + for (Stock s : stocks) { + if (s == null) { + continue; + } + + String csvRow = String.format("%s, %s, %s", + s.getSymbol().trim(), + s.getCompany().trim(), + s.getSalesPrice().toPlainString() ); - return stringList; + stringList.add(csvRow); } + return stringList; } } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java index 81d5f95..74f0470 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileManagerTest.java @@ -1,9 +1,7 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java index 61ecfb2..8fe5158 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/service/StockFileParserTest.java @@ -1,30 +1,37 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class StockFileParserTest { - private StockFileParser converter; + /** + * Test stock parser to use. + * */ + private StockFileParser testParser; - private String validStockAsString1; - private String validStockAsString2; - private String validStockAsString3; + /** + * List of strings containing valid and invalid representations + * of stocks. + * */ private ArrayList allStocks; - private String invalidStockAsString1; @BeforeEach void setUp() { - validStockAsString1 = "AAPL, Apple inc., 251.42"; - validStockAsString2 = "NVID, Nvidia corp., 100.25"; - validStockAsString3 = "SAMS, Samsung corporation, 103.21"; + String validStockAsString1 = "AAPL, Apple inc., 251.42"; + String validStockAsString2 = "NVID, Nvidia corp., 100.25"; + String validStockAsString3 = "SAMS, Samsung corporation, 103.21"; - invalidStockAsString1 = "INVALID, This stock has an invalid code!, 100.21"; + String invalidStockAsString1 = "INVALID, This stock has an invalid code!, 100.21"; allStocks = new ArrayList<>(); @@ -33,15 +40,15 @@ void setUp() { allStocks.add(validStockAsString3); allStocks.add(invalidStockAsString1); - converter = new StockFileParser(); + testParser = new StockFileParser(); } @Test - void converter_returns_valid_stock_apple() { + void getStocksFromStringsReturnsValidStocks() { boolean stockIncluded = false; - List stocksFromConverter = converter.getStocksFromStrings(allStocks); + List stocksFromConverter = testParser.getStocksFromStrings(allStocks); for (Stock s : stocksFromConverter) { if (s.getSymbol().equals("AAPL")) { @@ -54,11 +61,11 @@ void converter_returns_valid_stock_apple() { } @Test - void converter_ignores_invalid_stock_representation() { + void getStocksFromStringsIgnoresInvalidStocks() { boolean stockIncluded = false; - List stocksFromConverter = converter.getStocksFromStrings(allStocks); + List stocksFromConverter = testParser.getStocksFromStrings(allStocks); for (Stock s : stocksFromConverter) { if (s.getSymbol().equals("INVALID")) { @@ -69,4 +76,30 @@ void converter_ignores_invalid_stock_representation() { Assertions.assertFalse(stockIncluded); } + + @Test + void stocksToStringsConvertsValidStocksToCsvFormat() { + Stock apple = new Stock("AAPL", "Apple Inc", new BigDecimal("175.50")); + Stock tesla = new Stock("TSLA", "Tesla Inc", new BigDecimal("200.00")); + List stocks = List.of(apple, tesla); + + List result = testParser.stocksToStrings(stocks); + + assertEquals(2, result.size()); + assertEquals("AAPL, Apple Inc, 175.50", result.get(0)); + assertEquals("TSLA, Tesla Inc, 200.00", result.get(1)); + } + + @Test + void stocksToStringsThrowsExceptionOnNullOrEmptyList() { + List emptyList = new ArrayList<>(); + + assertThrows(IllegalArgumentException.class, + () -> testParser.stocksToStrings(null) + ); + + assertThrows(IllegalArgumentException.class, + () -> testParser.stocksToStrings(emptyList) + ); + } } \ No newline at end of file From 9310cae6edd0fc87fe8d701b1a0111539f17c5f8 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 21:19:29 +0200 Subject: [PATCH 30/31] Feat: Removed seetter method for viewname (unused) --- .../ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java | 10 ---------- .../ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java | 2 -- 2 files changed, 12 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java index d0c2ef0..77e7d8b 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewElement.java @@ -88,16 +88,6 @@ public ViewEnum getViewName() { return viewName; } - /** - * Setter method for the view name. - * - * @param name the new name to set this view element to. - * - */ - protected void setViewName(final ViewEnum name) { - viewName = name; - } - /** * Getter method for the root pane. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java index f6de33d..45da26b 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewManager.java @@ -6,8 +6,6 @@ import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import java.util.EnumMap; import java.util.Map; - -import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import javafx.scene.Scene; import javafx.stage.Stage; From d6d29ed873112b1ce89de79c6faa698be0dbfacc Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 21:39:37 +0200 Subject: [PATCH 31/31] Fix: Renamed method in Portfolio --- .../edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java | 2 +- .../mappe/view/widgets/dashboard/DashBoardController.java | 2 +- .../ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index 40a3afd..415a065 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -178,7 +178,7 @@ public BigDecimal getNetWorth() { * @return BigDecimal representing total quantity of all * shares of this symbol. * */ - public BigDecimal getTotalSharesBySymbol(final String symbol) { + public BigDecimal getTotalShareQuantityBySymbol(final String symbol) { if (symbol == null) { return BigDecimal.ZERO; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java index fa1333c..8bc66c5 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java @@ -116,7 +116,7 @@ private void populateStockList(final String filter) { getViewElement().setOnStockAction(stockBtn, s, (Stock stock) -> { BigDecimal amountOfSharesOwned = player.getPortfolio() - .getTotalSharesBySymbol(s.getSymbol()); + .getTotalShareQuantityBySymbol(s.getSymbol()); handleStockSelection(stock, amountOfSharesOwned.floatValue()); }); } diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java index 781e06f..fde5639 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/model/PortfolioTest.java @@ -131,13 +131,13 @@ void containsThrowsExceptionOnIllegalArgument() { void getTotalSharesBySymbolReturnsCorrectValues() { assertEquals(0, BigDecimal.ZERO.compareTo( - testPortfolio.getTotalSharesBySymbol("AAPL") + testPortfolio.getTotalShareQuantityBySymbol("AAPL") ) ); assertEquals(0, BigDecimal.ZERO.compareTo( - testPortfolio.getTotalSharesBySymbol(null) + testPortfolio.getTotalShareQuantityBySymbol(null) ) ); @@ -145,7 +145,7 @@ void getTotalSharesBySymbolReturnsCorrectValues() { assertEquals(0, new BigDecimal("2.0").compareTo( - testPortfolio.getTotalSharesBySymbol("AAPL") + testPortfolio.getTotalShareQuantityBySymbol("AAPL") ) ); }