init
This commit is contained in:
17
source/.gitignore
vendored
Normal file
17
source/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Maven build output
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Generated log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
*.classpath
|
||||||
|
*.project
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
51
source/pom.xml
Normal file
51
source/pom.xml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>se.kth.iv1350</groupId>
|
||||||
|
<artifactId>repairbike</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.10.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<sourceDirectory>src/main</sourceDirectory>
|
||||||
|
<testSourceDirectory>src/test</testSourceDirectory>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>startup.Main</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
103
source/src/main/controller/Controller.java
Normal file
103
source/src/main/controller/Controller.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package controller;
|
||||||
|
|
||||||
|
import integration.BikeNotFoundException;
|
||||||
|
import integration.BikeRegistry;
|
||||||
|
import integration.CustomerNotFoundException;
|
||||||
|
import integration.CustomerRegistry;
|
||||||
|
import integration.RepairTaskCatalog;
|
||||||
|
import integration.TaskNotFoundException;
|
||||||
|
import model.Amount;
|
||||||
|
import model.BikeDTO;
|
||||||
|
import model.CustomerDTO;
|
||||||
|
import model.RepairOrder;
|
||||||
|
import model.RepairShop;
|
||||||
|
import model.TaskDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates use-case execution between the view and the model and integration layers.
|
||||||
|
* All calls from the view pass through this class.
|
||||||
|
*/
|
||||||
|
public class Controller {
|
||||||
|
|
||||||
|
private final RepairShop repairShop;
|
||||||
|
private final BikeRegistry bikeRegistry;
|
||||||
|
private final RepairTaskCatalog taskCatalog;
|
||||||
|
private final CustomerRegistry customerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Controller wired to the given dependencies.
|
||||||
|
*
|
||||||
|
* @param repairShop The repair shop facade (model layer).
|
||||||
|
* @param bikeRegistry The bike registry (integration layer).
|
||||||
|
* @param taskCatalog The repair task catalog (integration layer).
|
||||||
|
* @param customerRegistry The customer registry (integration layer).
|
||||||
|
*/
|
||||||
|
public Controller(RepairShop repairShop, BikeRegistry bikeRegistry,
|
||||||
|
RepairTaskCatalog taskCatalog, CustomerRegistry customerRegistry) {
|
||||||
|
this.repairShop = repairShop;
|
||||||
|
this.bikeRegistry = bikeRegistry;
|
||||||
|
this.taskCatalog = taskCatalog;
|
||||||
|
this.customerRegistry = customerRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new repair session.
|
||||||
|
*/
|
||||||
|
public void startNewRepair() {
|
||||||
|
repairShop.startRepair();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a customer by phone number.
|
||||||
|
*
|
||||||
|
* @param phoneNumber The customer's phone number.
|
||||||
|
* @return The {@link CustomerDTO} of the found customer.
|
||||||
|
* @throws CustomerNotFoundException If no customer with that phone number exists.
|
||||||
|
*/
|
||||||
|
public CustomerDTO lookupCustomer(String phoneNumber) throws CustomerNotFoundException {
|
||||||
|
return customerRegistry.findCustomer(phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the bike with the given ID for the current repair session.
|
||||||
|
*
|
||||||
|
* @param bikeID The unique identifier of the bike to register.
|
||||||
|
* @return The {@link BikeDTO} of the registered bike.
|
||||||
|
* @throws BikeNotFoundException If no bike with the given ID exists in the registry.
|
||||||
|
*/
|
||||||
|
public BikeDTO enterBikeID(String bikeID) throws BikeNotFoundException {
|
||||||
|
BikeDTO bike = bikeRegistry.findBike(bikeID);
|
||||||
|
repairShop.registerBike(bike);
|
||||||
|
return bike;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the repair task with the given name to the current repair session.
|
||||||
|
*
|
||||||
|
* @param taskName The name of the repair task to add.
|
||||||
|
* @return The updated running total as an {@link Amount}.
|
||||||
|
* @throws TaskNotFoundException If no task with the given name exists in the catalog.
|
||||||
|
*/
|
||||||
|
public Amount addRepairTask(String taskName) throws TaskNotFoundException {
|
||||||
|
TaskDTO task = taskCatalog.findTask(taskName);
|
||||||
|
return repairShop.addTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the mechanic's diagnostic notes for the current repair session.
|
||||||
|
*
|
||||||
|
* @param report The diagnostic report text entered by the mechanic.
|
||||||
|
*/
|
||||||
|
public void enterDiagnosticReport(String report) {
|
||||||
|
repairShop.enterDiagnosticReport(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the current repair session and returns the completed repair order.
|
||||||
|
*
|
||||||
|
* @return The {@link RepairOrder} summarising all repair details.
|
||||||
|
*/
|
||||||
|
public RepairOrder endRepair() {
|
||||||
|
return repairShop.endRepair();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
source/src/main/integration/BikeNotFoundException.java
Normal file
30
source/src/main/integration/BikeNotFoundException.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a bike ID search yields no match in the bike registry.
|
||||||
|
* This is a checked exception because the caller is expected to handle the case
|
||||||
|
* where a bike is not found and prompt the user to re-enter a valid ID.
|
||||||
|
*/
|
||||||
|
public class BikeNotFoundException extends Exception {
|
||||||
|
|
||||||
|
private final String bikeID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance indicating which bike ID was not found.
|
||||||
|
*
|
||||||
|
* @param bikeID The bike ID that had no matching record.
|
||||||
|
*/
|
||||||
|
public BikeNotFoundException(String bikeID) {
|
||||||
|
super("No bike found with ID: " + bikeID);
|
||||||
|
this.bikeID = bikeID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the bike ID that was not found in the registry.
|
||||||
|
*
|
||||||
|
* @return The missing bike ID string.
|
||||||
|
*/
|
||||||
|
public String getBikeID() {
|
||||||
|
return bikeID;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
source/src/main/integration/BikeRegistry.java
Normal file
66
source/src/main/integration/BikeRegistry.java
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
import model.BikeDTO;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles retrieval of bike information from persistent storage.
|
||||||
|
* Implemented as a Singleton to ensure only one instance of the registry exists
|
||||||
|
* throughout the application's lifetime, guaranteeing a single consistent data source.
|
||||||
|
* In this implementation, bikes are stored in memory as sample data.
|
||||||
|
*
|
||||||
|
* <p>The bike ID {@code "DB-FAIL-999"} is hardcoded to always trigger a
|
||||||
|
* {@link DatabaseFailureException}, simulating an unavailable database server.</p>
|
||||||
|
*/
|
||||||
|
public class BikeRegistry {
|
||||||
|
|
||||||
|
/** Hardcoded ID that always causes a simulated database failure. */
|
||||||
|
public static final String DB_FAILURE_ID = "DB-FAIL-999";
|
||||||
|
|
||||||
|
private static BikeRegistry instance;
|
||||||
|
private final Map<String, BikeDTO> bikes = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor — use {@link #getInstance()} to obtain the single instance.
|
||||||
|
*/
|
||||||
|
private BikeRegistry() {
|
||||||
|
bikes.put("BIKE-001", new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
bikes.put("BIKE-002", new BikeDTO("BIKE-002", "Bob Lindqvist"));
|
||||||
|
bikes.put("BIKE-003", new BikeDTO("BIKE-003", "Carl Johansson"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the single shared instance of {@code BikeRegistry}, creating it on
|
||||||
|
* first call.
|
||||||
|
*
|
||||||
|
* @return The singleton {@code BikeRegistry} instance.
|
||||||
|
*/
|
||||||
|
public static BikeRegistry getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new BikeRegistry();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up and returns the bike with the given ID.
|
||||||
|
*
|
||||||
|
* @param bikeID The unique identifier of the bike to look up.
|
||||||
|
* @return The {@link BikeDTO} of the found bike.
|
||||||
|
* @throws BikeNotFoundException If no bike with the given ID exists in the registry.
|
||||||
|
* @throws DatabaseFailureException If the bike ID equals the hardcoded failure trigger,
|
||||||
|
* simulating an unavailable database server.
|
||||||
|
*/
|
||||||
|
public BikeDTO findBike(String bikeID) throws BikeNotFoundException {
|
||||||
|
if (DB_FAILURE_ID.equals(bikeID)) {
|
||||||
|
throw new DatabaseFailureException("BikeRegistry.findBike(\"" + bikeID + "\")");
|
||||||
|
}
|
||||||
|
BikeDTO bike = bikes.get(bikeID);
|
||||||
|
if (bike == null) {
|
||||||
|
throw new BikeNotFoundException(bikeID);
|
||||||
|
}
|
||||||
|
return bike;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
source/src/main/integration/CustomerNotFoundException.java
Normal file
30
source/src/main/integration/CustomerNotFoundException.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a customer phone number search yields no match in the customer registry.
|
||||||
|
* This is a checked exception because the caller is expected to handle the case
|
||||||
|
* where a customer is not found and inform the user accordingly.
|
||||||
|
*/
|
||||||
|
public class CustomerNotFoundException extends Exception {
|
||||||
|
|
||||||
|
private final String phoneNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance indicating which phone number was not found.
|
||||||
|
*
|
||||||
|
* @param phoneNumber The phone number that had no matching customer record.
|
||||||
|
*/
|
||||||
|
public CustomerNotFoundException(String phoneNumber) {
|
||||||
|
super("No customer found with phone number: " + phoneNumber);
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the phone number that was not found in the registry.
|
||||||
|
*
|
||||||
|
* @return The missing phone number string.
|
||||||
|
*/
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
source/src/main/integration/CustomerRegistry.java
Normal file
41
source/src/main/integration/CustomerRegistry.java
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
import model.CustomerDTO;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles retrieval of customer information from persistent storage.
|
||||||
|
* Customers are looked up by their phone number. In this implementation,
|
||||||
|
* customers are stored in memory as sample data.
|
||||||
|
*/
|
||||||
|
public class CustomerRegistry {
|
||||||
|
|
||||||
|
private final Map<String, CustomerDTO> customers = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CustomerRegistry pre-loaded with sample customer records.
|
||||||
|
*/
|
||||||
|
public CustomerRegistry() {
|
||||||
|
customers.put("0701234567", new CustomerDTO("0701234567", "Alice Svensson"));
|
||||||
|
customers.put("0709876543", new CustomerDTO("0709876543", "Bob Lindqvist"));
|
||||||
|
customers.put("0705551234", new CustomerDTO("0705551234", "Carl Johansson"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up and returns the customer with the given phone number.
|
||||||
|
*
|
||||||
|
* @param phoneNumber The phone number of the customer to look up.
|
||||||
|
* @return The {@link CustomerDTO} of the found customer.
|
||||||
|
* @throws CustomerNotFoundException If no customer with the given phone number
|
||||||
|
* exists in the registry.
|
||||||
|
*/
|
||||||
|
public CustomerDTO findCustomer(String phoneNumber) throws CustomerNotFoundException {
|
||||||
|
CustomerDTO customer = customers.get(phoneNumber);
|
||||||
|
if (customer == null) {
|
||||||
|
throw new CustomerNotFoundException(phoneNumber);
|
||||||
|
}
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
source/src/main/integration/DatabaseFailureException.java
Normal file
44
source/src/main/integration/DatabaseFailureException.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the underlying database or data source cannot be reached.
|
||||||
|
* This is an unchecked exception because a database failure is an infrastructure
|
||||||
|
* fault that the application cannot meaningfully recover from at runtime. Callers
|
||||||
|
* should catch this exception only at the outermost layer (the view) in order to
|
||||||
|
* log the failure and notify the user that the system is temporarily unavailable.
|
||||||
|
*/
|
||||||
|
public class DatabaseFailureException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String operation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance describing which database operation failed.
|
||||||
|
*
|
||||||
|
* @param operation A short description of the operation that caused the failure
|
||||||
|
* (e.g., {@code "BikeRegistry.findBike"}).
|
||||||
|
*/
|
||||||
|
public DatabaseFailureException(String operation) {
|
||||||
|
super("Database failure during operation: " + operation);
|
||||||
|
this.operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance with a root-cause exception.
|
||||||
|
*
|
||||||
|
* @param operation A short description of the operation that caused the failure.
|
||||||
|
* @param cause The underlying throwable that triggered this exception.
|
||||||
|
*/
|
||||||
|
public DatabaseFailureException(String operation, Throwable cause) {
|
||||||
|
super("Database failure during operation: " + operation, cause);
|
||||||
|
this.operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a description of the database operation that failed.
|
||||||
|
*
|
||||||
|
* @return The operation string.
|
||||||
|
*/
|
||||||
|
public String getOperation() {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
source/src/main/integration/RepairTaskCatalog.java
Normal file
41
source/src/main/integration/RepairTaskCatalog.java
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
import model.Amount;
|
||||||
|
import model.TaskDTO;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles retrieval of repair task information from persistent storage.
|
||||||
|
* In this implementation, tasks are stored in memory as sample data.
|
||||||
|
*/
|
||||||
|
public class RepairTaskCatalog {
|
||||||
|
|
||||||
|
private final Map<String, TaskDTO> tasks = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a RepairTaskCatalog pre-loaded with sample repair task records.
|
||||||
|
*/
|
||||||
|
public RepairTaskCatalog() {
|
||||||
|
tasks.put("Brake Pad Replacement", new TaskDTO("Brake Pad Replacement", new Amount(350.00)));
|
||||||
|
tasks.put("Tire Replacement", new TaskDTO("Tire Replacement", new Amount(500.00)));
|
||||||
|
tasks.put("Battery Check", new TaskDTO("Battery Check", new Amount(200.00)));
|
||||||
|
tasks.put("Chain Lubrication", new TaskDTO("Chain Lubrication", new Amount(150.00)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up and returns the repair task with the given name.
|
||||||
|
*
|
||||||
|
* @param taskName The name of the repair task to look up.
|
||||||
|
* @return The {@link TaskDTO} of the found task.
|
||||||
|
* @throws TaskNotFoundException If no task with the given name exists in the catalog.
|
||||||
|
*/
|
||||||
|
public TaskDTO findTask(String taskName) throws TaskNotFoundException {
|
||||||
|
TaskDTO task = tasks.get(taskName);
|
||||||
|
if (task == null) {
|
||||||
|
throw new TaskNotFoundException(taskName);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
source/src/main/integration/TaskNotFoundException.java
Normal file
30
source/src/main/integration/TaskNotFoundException.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a repair task name search yields no match in the task catalog.
|
||||||
|
* This is a checked exception because the caller is expected to handle the case
|
||||||
|
* where a task is not found and prompt the user to re-enter a valid task name.
|
||||||
|
*/
|
||||||
|
public class TaskNotFoundException extends Exception {
|
||||||
|
|
||||||
|
private final String taskName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance indicating which task name was not found.
|
||||||
|
*
|
||||||
|
* @param taskName The task name that had no matching record.
|
||||||
|
*/
|
||||||
|
public TaskNotFoundException(String taskName) {
|
||||||
|
super("No repair task found with name: " + taskName);
|
||||||
|
this.taskName = taskName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the task name that was not found in the catalog.
|
||||||
|
*
|
||||||
|
* @return The missing task name string.
|
||||||
|
*/
|
||||||
|
public String getTaskName() {
|
||||||
|
return taskName;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
source/src/main/model/ActiveRepair.java
Normal file
117
source/src/main/model/ActiveRepair.java
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents one ongoing repair session for a single bike.
|
||||||
|
* Only accessible within the model package via {@link RepairShop}.
|
||||||
|
*
|
||||||
|
* <p>State invariant: the object never partially changes state when an operation
|
||||||
|
* fails. All modifications are applied atomically — if any step throws, the
|
||||||
|
* previous state is preserved.</p>
|
||||||
|
*/
|
||||||
|
class ActiveRepair {
|
||||||
|
|
||||||
|
private BikeDTO bike;
|
||||||
|
private final List<TaskDTO> tasks = new ArrayList<>();
|
||||||
|
private String diagnosticReport = "";
|
||||||
|
private Amount total = new Amount(0);
|
||||||
|
private DiscountStrategy discountStrategy;
|
||||||
|
private final List<RepairOrderObserver> observers = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ActiveRepair with no discount and no observers.
|
||||||
|
* Observers and a discount strategy can be added after construction.
|
||||||
|
*/
|
||||||
|
ActiveRepair() {
|
||||||
|
this.discountStrategy = new NoDiscount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an observer that will be notified on every state change.
|
||||||
|
*
|
||||||
|
* @param observer The observer to add.
|
||||||
|
*/
|
||||||
|
void addObserver(RepairOrderObserver observer) {
|
||||||
|
observers.add(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the discount strategy to apply when calculating the repair total.
|
||||||
|
*
|
||||||
|
* @param strategy The discount strategy to use; must not be {@code null}.
|
||||||
|
*/
|
||||||
|
void setDiscountStrategy(DiscountStrategy strategy) {
|
||||||
|
this.discountStrategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the bike that is being repaired in this session and notifies observers.
|
||||||
|
*
|
||||||
|
* @param bike The bike to register.
|
||||||
|
*/
|
||||||
|
void registerBike(BikeDTO bike) {
|
||||||
|
this.bike = bike;
|
||||||
|
notifyObservers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a repair task to this session, recalculates the total, and notifies observers.
|
||||||
|
* If an error occurs the task list and total remain unchanged.
|
||||||
|
*
|
||||||
|
* @param task The repair task to add.
|
||||||
|
* @return The new running total after adding the task.
|
||||||
|
*/
|
||||||
|
Amount addTask(TaskDTO task) {
|
||||||
|
tasks.add(task);
|
||||||
|
total = calculateTotal();
|
||||||
|
notifyObservers();
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the mechanic's diagnostic notes and notifies observers.
|
||||||
|
*
|
||||||
|
* @param report The diagnostic report text entered by the mechanic.
|
||||||
|
*/
|
||||||
|
void enterDiagnosticReport(String report) {
|
||||||
|
this.diagnosticReport = report;
|
||||||
|
notifyObservers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the repair session, applies the discount strategy, notifies observers,
|
||||||
|
* and returns a completed repair order.
|
||||||
|
*
|
||||||
|
* @return The {@link RepairOrder} summarising all repair details.
|
||||||
|
*/
|
||||||
|
RepairOrder endRepair() {
|
||||||
|
Amount discountedTotal = discountStrategy.applyDiscount(total);
|
||||||
|
List<TaskDTO> immutableTasks = Collections.unmodifiableList(new ArrayList<>(tasks));
|
||||||
|
RepairOrder repairOrder = new RepairOrder(bike, immutableTasks, diagnosticReport, discountedTotal);
|
||||||
|
notifyObservers(repairOrder);
|
||||||
|
return repairOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Amount calculateTotal() {
|
||||||
|
Amount sum = new Amount(0);
|
||||||
|
for (TaskDTO task : tasks) {
|
||||||
|
sum = sum.add(task.getCost());
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyObservers() {
|
||||||
|
List<TaskDTO> snapshot = Collections.unmodifiableList(new ArrayList<>(tasks));
|
||||||
|
RepairOrder currentState = new RepairOrder(bike, snapshot, diagnosticReport, total);
|
||||||
|
notifyObservers(currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyObservers(RepairOrder repairOrder) {
|
||||||
|
for (RepairOrderObserver observer : observers) {
|
||||||
|
observer.repairOrderUpdated(repairOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
source/src/main/model/Amount.java
Normal file
66
source/src/main/model/Amount.java
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a monetary amount. Immutable; all arithmetic returns new instances.
|
||||||
|
*/
|
||||||
|
public class Amount {
|
||||||
|
private final double value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Amount with the specified value.
|
||||||
|
*
|
||||||
|
* @param value The monetary value.
|
||||||
|
*/
|
||||||
|
public Amount(double value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new Amount that is the sum of this amount and the given amount.
|
||||||
|
*
|
||||||
|
* @param other The amount to add.
|
||||||
|
* @return A new Amount representing the sum.
|
||||||
|
*/
|
||||||
|
public Amount add(Amount other) {
|
||||||
|
return new Amount(this.value + other.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new Amount that is the difference of this amount minus the given amount.
|
||||||
|
*
|
||||||
|
* @param other The amount to subtract.
|
||||||
|
* @return A new Amount representing the difference.
|
||||||
|
*/
|
||||||
|
public Amount subtract(Amount other) {
|
||||||
|
return new Amount(this.value - other.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new Amount scaled by the given factor.
|
||||||
|
*
|
||||||
|
* @param factor The multiplier to apply.
|
||||||
|
* @return A new Amount representing the scaled value.
|
||||||
|
*/
|
||||||
|
public Amount multiply(double factor) {
|
||||||
|
return new Amount(this.value * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw numeric value of this amount.
|
||||||
|
*
|
||||||
|
* @return The monetary value as a double.
|
||||||
|
*/
|
||||||
|
public double getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a formatted string representation of this amount.
|
||||||
|
*
|
||||||
|
* @return A string in the form {@code "123.45 SEK"}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%.2f SEK", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
source/src/main/model/BikeDTO.java
Normal file
39
source/src/main/model/BikeDTO.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object that carries bike information across layer boundaries.
|
||||||
|
* Instances are immutable.
|
||||||
|
*/
|
||||||
|
public class BikeDTO {
|
||||||
|
private final String bikeID;
|
||||||
|
private final String ownerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BikeDTO with the specified bike ID and owner name.
|
||||||
|
*
|
||||||
|
* @param bikeID The unique identifier of the bike.
|
||||||
|
* @param ownerName The full name of the bike's owner.
|
||||||
|
*/
|
||||||
|
public BikeDTO(String bikeID, String ownerName) {
|
||||||
|
this.bikeID = bikeID;
|
||||||
|
this.ownerName = ownerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the bike's unique identifier.
|
||||||
|
*
|
||||||
|
* @return The bike ID string.
|
||||||
|
*/
|
||||||
|
public String getBikeID() {
|
||||||
|
return bikeID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full name of the bike's owner.
|
||||||
|
*
|
||||||
|
* @return The owner's name.
|
||||||
|
*/
|
||||||
|
public String getOwnerName() {
|
||||||
|
return ownerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
source/src/main/model/CustomerDTO.java
Normal file
39
source/src/main/model/CustomerDTO.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object that carries customer information across layer boundaries.
|
||||||
|
* Instances are immutable.
|
||||||
|
*/
|
||||||
|
public class CustomerDTO {
|
||||||
|
private final String phoneNumber;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CustomerDTO with the specified phone number and name.
|
||||||
|
*
|
||||||
|
* @param phoneNumber The customer's phone number.
|
||||||
|
* @param name The full name of the customer.
|
||||||
|
*/
|
||||||
|
public CustomerDTO(String phoneNumber, String name) {
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the customer's phone number.
|
||||||
|
*
|
||||||
|
* @return The phone number string.
|
||||||
|
*/
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the customer's full name.
|
||||||
|
*
|
||||||
|
* @return The name string.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
source/src/main/model/DiscountStrategy.java
Normal file
17
source/src/main/model/DiscountStrategy.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy interface for calculating customer discounts on a repair total.
|
||||||
|
* Different discount rules (loyalty, seasonal, etc.) are each implemented as a
|
||||||
|
* separate class that implements this interface.
|
||||||
|
*/
|
||||||
|
public interface DiscountStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates and returns the discounted total for the given amount.
|
||||||
|
*
|
||||||
|
* @param total The original repair total before any discount.
|
||||||
|
* @return The total after applying the discount.
|
||||||
|
*/
|
||||||
|
Amount applyDiscount(Amount total);
|
||||||
|
}
|
||||||
22
source/src/main/model/LoyaltyDiscount.java
Normal file
22
source/src/main/model/LoyaltyDiscount.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A discount strategy that grants loyal customers a 10% reduction on the total
|
||||||
|
* repair cost. This strategy can be applied when a customer qualifies for the
|
||||||
|
* loyalty programme (e.g. every third repair).
|
||||||
|
*/
|
||||||
|
public class LoyaltyDiscount implements DiscountStrategy {
|
||||||
|
|
||||||
|
private static final double DISCOUNT_RATE = 0.10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the given total reduced by {@value #DISCOUNT_RATE} (10%).
|
||||||
|
*
|
||||||
|
* @param total The original repair total.
|
||||||
|
* @return The total after applying a 10% loyalty discount.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Amount applyDiscount(Amount total) {
|
||||||
|
return total.multiply(1.0 - DISCOUNT_RATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
source/src/main/model/NoDiscount.java
Normal file
20
source/src/main/model/NoDiscount.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A discount strategy that applies no discount. The original total is returned
|
||||||
|
* unchanged. This is the default strategy used when the customer has no active
|
||||||
|
* discount.
|
||||||
|
*/
|
||||||
|
public class NoDiscount implements DiscountStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the given total unchanged.
|
||||||
|
*
|
||||||
|
* @param total The original repair total.
|
||||||
|
* @return The same total, with no reduction applied.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Amount applyDiscount(Amount total) {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
source/src/main/model/RepairOrder.java
Normal file
67
source/src/main/model/RepairOrder.java
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of a repair order, either as an in-progress snapshot or
|
||||||
|
* as a completed order issued at the end of a repair session. Instances are immutable.
|
||||||
|
*/
|
||||||
|
public class RepairOrder {
|
||||||
|
private final BikeDTO bike;
|
||||||
|
private final List<TaskDTO> tasks;
|
||||||
|
private final String diagnosticReport;
|
||||||
|
private final Amount total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a RepairOrder with the given repair details.
|
||||||
|
*
|
||||||
|
* @param bike The bike being repaired (may be {@code null} if the
|
||||||
|
* bike has not yet been registered in this session).
|
||||||
|
* @param tasks The list of repair tasks performed so far.
|
||||||
|
* @param diagnosticReport The mechanic's diagnostic notes (may be empty).
|
||||||
|
* @param total The total cost of all tasks added so far.
|
||||||
|
*/
|
||||||
|
RepairOrder(BikeDTO bike, List<TaskDTO> tasks, String diagnosticReport, Amount total) {
|
||||||
|
this.bike = bike;
|
||||||
|
this.tasks = tasks;
|
||||||
|
this.diagnosticReport = diagnosticReport;
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the bike information for this repair order.
|
||||||
|
*
|
||||||
|
* @return The {@link BikeDTO} of the repaired bike, or {@code null} if the bike
|
||||||
|
* has not yet been registered.
|
||||||
|
*/
|
||||||
|
public BikeDTO getBike() {
|
||||||
|
return bike;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of repair tasks included in this order.
|
||||||
|
*
|
||||||
|
* @return An unmodifiable list of {@link TaskDTO} objects.
|
||||||
|
*/
|
||||||
|
public List<TaskDTO> getTasks() {
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the mechanic's diagnostic report for this repair.
|
||||||
|
*
|
||||||
|
* @return The diagnostic report string.
|
||||||
|
*/
|
||||||
|
public String getDiagnosticReport() {
|
||||||
|
return diagnosticReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total cost of all repair tasks in this order.
|
||||||
|
*
|
||||||
|
* @return The total as an {@link Amount}.
|
||||||
|
*/
|
||||||
|
public Amount getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
source/src/main/model/RepairOrderObserver.java
Normal file
16
source/src/main/model/RepairOrderObserver.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer interface for repair order updates.
|
||||||
|
* Any class that wants to be notified whenever a repair order changes must
|
||||||
|
* implement this interface and register itself with {@link RepairShop}.
|
||||||
|
*/
|
||||||
|
public interface RepairOrderObserver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a repair order is created or updated.
|
||||||
|
*
|
||||||
|
* @param repairOrder A snapshot of the current repair order state.
|
||||||
|
*/
|
||||||
|
void repairOrderUpdated(RepairOrder repairOrder);
|
||||||
|
}
|
||||||
91
source/src/main/model/RepairShop.java
Normal file
91
source/src/main/model/RepairShop.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facade for the model layer. Manages the lifecycle of one repair session at a time
|
||||||
|
* and delegates all operations to the current {@link ActiveRepair}.
|
||||||
|
*
|
||||||
|
* <p>Observers registered via {@link #addObserver(RepairOrderObserver)} are
|
||||||
|
* automatically forwarded to every new repair session that is started.</p>
|
||||||
|
*/
|
||||||
|
public class RepairShop {
|
||||||
|
|
||||||
|
private ActiveRepair currentRepair;
|
||||||
|
private final List<RepairOrderObserver> observers = new ArrayList<>();
|
||||||
|
private DiscountStrategy discountStrategy = new NoDiscount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RepairShop ready to accept repair sessions.
|
||||||
|
*/
|
||||||
|
public RepairShop() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an observer to be notified whenever a repair order changes.
|
||||||
|
* The observer is passed to every repair session started after this call.
|
||||||
|
*
|
||||||
|
* @param observer The observer to register.
|
||||||
|
*/
|
||||||
|
public void addObserver(RepairOrderObserver observer) {
|
||||||
|
observers.add(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the discount strategy to apply to each new repair session's total.
|
||||||
|
*
|
||||||
|
* @param strategy The discount strategy; must not be {@code null}.
|
||||||
|
*/
|
||||||
|
public void setDiscountStrategy(DiscountStrategy strategy) {
|
||||||
|
this.discountStrategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new repair session, discarding any previously active session.
|
||||||
|
*/
|
||||||
|
public void startRepair() {
|
||||||
|
currentRepair = new ActiveRepair();
|
||||||
|
for (RepairOrderObserver observer : observers) {
|
||||||
|
currentRepair.addObserver(observer);
|
||||||
|
}
|
||||||
|
currentRepair.setDiscountStrategy(discountStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the bike that will be worked on during the current repair session.
|
||||||
|
*
|
||||||
|
* @param bike The {@link BikeDTO} of the bike to register.
|
||||||
|
*/
|
||||||
|
public void registerBike(BikeDTO bike) {
|
||||||
|
currentRepair.registerBike(bike);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a repair task to the current session and returns the updated running total.
|
||||||
|
*
|
||||||
|
* @param task The {@link TaskDTO} of the task to add.
|
||||||
|
* @return The running total after the task has been added.
|
||||||
|
*/
|
||||||
|
public Amount addTask(TaskDTO task) {
|
||||||
|
return currentRepair.addTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the mechanic's diagnostic notes for the current repair session.
|
||||||
|
*
|
||||||
|
* @param report The diagnostic report text entered by the mechanic.
|
||||||
|
*/
|
||||||
|
public void enterDiagnosticReport(String report) {
|
||||||
|
currentRepair.enterDiagnosticReport(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the current repair session and returns the completed repair order.
|
||||||
|
*
|
||||||
|
* @return The {@link RepairOrder} for the finished repair.
|
||||||
|
*/
|
||||||
|
public RepairOrder endRepair() {
|
||||||
|
return currentRepair.endRepair();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
source/src/main/model/TaskDTO.java
Normal file
39
source/src/main/model/TaskDTO.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object that carries repair task information across layer boundaries.
|
||||||
|
* Instances are immutable.
|
||||||
|
*/
|
||||||
|
public class TaskDTO {
|
||||||
|
private final String name;
|
||||||
|
private final Amount cost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a TaskDTO with the specified task name and cost.
|
||||||
|
*
|
||||||
|
* @param name The name of the repair task.
|
||||||
|
* @param cost The cost of the repair task.
|
||||||
|
*/
|
||||||
|
public TaskDTO(String name, Amount cost) {
|
||||||
|
this.name = name;
|
||||||
|
this.cost = cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the repair task name.
|
||||||
|
*
|
||||||
|
* @return The task name string.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cost of this repair task.
|
||||||
|
*
|
||||||
|
* @return The cost as an {@link Amount}.
|
||||||
|
*/
|
||||||
|
public Amount getCost() {
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
source/src/main/startup/Main.java
Normal file
41
source/src/main/startup/Main.java
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package startup;
|
||||||
|
|
||||||
|
import controller.Controller;
|
||||||
|
import integration.BikeRegistry;
|
||||||
|
import integration.CustomerRegistry;
|
||||||
|
import integration.RepairTaskCatalog;
|
||||||
|
import model.LoyaltyDiscount;
|
||||||
|
import model.RepairShop;
|
||||||
|
import view.RepairOrderLogger;
|
||||||
|
import view.RepairOrderView;
|
||||||
|
import view.View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the application entry point. Responsible for creating and wiring
|
||||||
|
* all top-level objects in dependency order.
|
||||||
|
*/
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the Repair Electric Bike application.
|
||||||
|
*
|
||||||
|
* @param args Command-line arguments (not used).
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Integration layer
|
||||||
|
BikeRegistry bikeRegistry = BikeRegistry.getInstance(); // Singleton
|
||||||
|
RepairTaskCatalog taskCatalog = new RepairTaskCatalog();
|
||||||
|
CustomerRegistry customerRegistry = new CustomerRegistry();
|
||||||
|
|
||||||
|
// Model layer — wire observers and discount strategy before first session
|
||||||
|
RepairShop repairShop = new RepairShop();
|
||||||
|
repairShop.addObserver(new RepairOrderView());
|
||||||
|
repairShop.addObserver(new RepairOrderLogger());
|
||||||
|
repairShop.setDiscountStrategy(new LoyaltyDiscount()); // Strategy pattern
|
||||||
|
|
||||||
|
// Controller and view
|
||||||
|
Controller controller = new Controller(repairShop, bikeRegistry, taskCatalog, customerRegistry);
|
||||||
|
View view = new View(controller);
|
||||||
|
view.runFakeExecution();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
source/src/main/util/ErrorLogger.java
Normal file
50
source/src/main/util/ErrorLogger.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package util;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs error information about unexpected exceptions to a dedicated error log file.
|
||||||
|
* This logger is intended for developer-level diagnostics: whenever an exception
|
||||||
|
* indicates that the program is not functioning as intended, the view catches it and
|
||||||
|
* calls {@link #logException(Exception)} to record a timestamped full report.
|
||||||
|
* Following the pattern in listing 8.25 of the course textbook, only exceptions that
|
||||||
|
* are <em>not</em> business-logic errors are logged here.
|
||||||
|
*/
|
||||||
|
public class ErrorLogger {
|
||||||
|
|
||||||
|
private static final FileLogger FILE_LOGGER = new FileLogger("error.log");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor — this class is a utility class and should not be instantiated.
|
||||||
|
*/
|
||||||
|
private ErrorLogger() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the exception message and its full stack trace to {@code error.log}.
|
||||||
|
* The log entry begins with a timestamp followed by the exception message, then
|
||||||
|
* the complete stack trace, following the pattern from listing 8.25 of the textbook.
|
||||||
|
*
|
||||||
|
* @param exception The exception that indicates a program fault.
|
||||||
|
*/
|
||||||
|
public static void logException(Exception exception) {
|
||||||
|
StringBuilder logMsgBuilder = new StringBuilder();
|
||||||
|
logMsgBuilder.append(createTime());
|
||||||
|
logMsgBuilder.append(", Exception was thrown: ");
|
||||||
|
logMsgBuilder.append(exception.getMessage());
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
exception.printStackTrace(new PrintWriter(sw));
|
||||||
|
logMsgBuilder.append("\n").append(sw.toString());
|
||||||
|
FILE_LOGGER.log(logMsgBuilder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createTime() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
|
||||||
|
return now.format(formatter);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
source/src/main/util/FileLogger.java
Normal file
48
source/src/main/util/FileLogger.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package util;
|
||||||
|
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes timestamped log messages to a text file. The log stream is opened once
|
||||||
|
* in the constructor and kept open for the lifetime of this object, following the
|
||||||
|
* pattern illustrated in listing 9.1 of the course textbook.
|
||||||
|
*/
|
||||||
|
public class FileLogger {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private PrintWriter logStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a FileLogger that appends to the specified file. If the file does not
|
||||||
|
* exist it is created. If the log stream cannot be opened, an error is printed
|
||||||
|
* to standard output and subsequent calls to {@link #log(String)} are silently
|
||||||
|
* ignored.
|
||||||
|
*
|
||||||
|
* @param filename The path to the log file.
|
||||||
|
*/
|
||||||
|
public FileLogger(String filename) {
|
||||||
|
try {
|
||||||
|
logStream = new PrintWriter(new FileWriter(filename, true), true);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
System.out.println("CAN NOT LOG.");
|
||||||
|
ioe.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a timestamped line to the log file.
|
||||||
|
*
|
||||||
|
* @param message The message to log.
|
||||||
|
*/
|
||||||
|
public void log(String message) {
|
||||||
|
if (logStream != null) {
|
||||||
|
logStream.println("[" + LocalDateTime.now().format(FORMATTER) + "] " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
source/src/main/view/RepairOrderLogger.java
Normal file
59
source/src/main/view/RepairOrderLogger.java
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package view;
|
||||||
|
|
||||||
|
import model.RepairOrder;
|
||||||
|
import model.RepairOrderObserver;
|
||||||
|
import model.TaskDTO;
|
||||||
|
import util.FileLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observer that writes the current repair order to a log file whenever it is
|
||||||
|
* updated. This provides a persistent audit trail of all repair order state changes.
|
||||||
|
*/
|
||||||
|
public class RepairOrderLogger implements RepairOrderObserver {
|
||||||
|
|
||||||
|
private final FileLogger fileLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a RepairOrderLogger that appends to {@code repair-orders.log}.
|
||||||
|
*/
|
||||||
|
public RepairOrderLogger() {
|
||||||
|
this.fileLogger = new FileLogger("repair-orders.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a repair order is created or updated.
|
||||||
|
* Writes the full contents of the repair order to the log file.
|
||||||
|
*
|
||||||
|
* @param repairOrder A snapshot of the current repair order state.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void repairOrderUpdated(RepairOrder repairOrder) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("RepairOrder updated | ");
|
||||||
|
sb.append("Bike: ").append(
|
||||||
|
repairOrder.getBike() != null
|
||||||
|
? repairOrder.getBike().getBikeID()
|
||||||
|
: "N/A");
|
||||||
|
sb.append(" | Tasks: ").append(formatTasks(repairOrder));
|
||||||
|
sb.append(" | Total: ").append(repairOrder.getTotal());
|
||||||
|
sb.append(" | Report: ").append(
|
||||||
|
repairOrder.getDiagnosticReport().isEmpty()
|
||||||
|
? "(none)"
|
||||||
|
: repairOrder.getDiagnosticReport());
|
||||||
|
fileLogger.log(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTasks(RepairOrder repairOrder) {
|
||||||
|
if (repairOrder.getTasks().isEmpty()) {
|
||||||
|
return "(none)";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (TaskDTO task : repairOrder.getTasks()) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
sb.append(task.getName());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
source/src/main/view/RepairOrderView.java
Normal file
49
source/src/main/view/RepairOrderView.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package view;
|
||||||
|
|
||||||
|
import model.RepairOrder;
|
||||||
|
import model.RepairOrderObserver;
|
||||||
|
import model.TaskDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observer that prints the current repair order to standard output whenever
|
||||||
|
* it is updated. This view replaces the need for a technician or receptionist to
|
||||||
|
* manually query the system for the latest repair order state.
|
||||||
|
*/
|
||||||
|
public class RepairOrderView implements RepairOrderObserver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a repair order is created or updated.
|
||||||
|
* Prints the full contents of the repair order to {@code System.out}.
|
||||||
|
*
|
||||||
|
* @param repairOrder A snapshot of the current repair order state.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void repairOrderUpdated(RepairOrder repairOrder) {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(">>> [RepairOrderView] Repair order updated <<<");
|
||||||
|
System.out.println(" Bike : "
|
||||||
|
+ (repairOrder.getBike() != null
|
||||||
|
? repairOrder.getBike().getBikeID() + " (" + repairOrder.getBike().getOwnerName() + ")"
|
||||||
|
: "(not yet registered)"));
|
||||||
|
System.out.println(" Tasks : " + formatTasks(repairOrder));
|
||||||
|
System.out.println(" Total : " + repairOrder.getTotal());
|
||||||
|
System.out.println(" Report : "
|
||||||
|
+ (repairOrder.getDiagnosticReport().isEmpty()
|
||||||
|
? "(none)"
|
||||||
|
: repairOrder.getDiagnosticReport()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTasks(RepairOrder repairOrder) {
|
||||||
|
if (repairOrder.getTasks().isEmpty()) {
|
||||||
|
return "(none)";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (TaskDTO task : repairOrder.getTasks()) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
sb.append(task.getName()).append(" (").append(task.getCost()).append(")");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
163
source/src/main/view/View.java
Normal file
163
source/src/main/view/View.java
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package view;
|
||||||
|
|
||||||
|
import controller.Controller;
|
||||||
|
import integration.BikeNotFoundException;
|
||||||
|
import integration.CustomerNotFoundException;
|
||||||
|
import integration.DatabaseFailureException;
|
||||||
|
import integration.TaskNotFoundException;
|
||||||
|
import model.Amount;
|
||||||
|
import model.BikeDTO;
|
||||||
|
import model.CustomerDTO;
|
||||||
|
import model.RepairOrder;
|
||||||
|
import model.TaskDTO;
|
||||||
|
import util.ErrorLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder view that simulates mechanic and receptionist interactions using
|
||||||
|
* hard-coded method calls. All output is printed to standard output. Caught
|
||||||
|
* exceptions that indicate user errors are shown as informative messages;
|
||||||
|
* exceptions that indicate system faults are additionally written to the error log.
|
||||||
|
*/
|
||||||
|
public class View {
|
||||||
|
|
||||||
|
private final Controller controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a View connected to the given controller.
|
||||||
|
*
|
||||||
|
* @param controller The controller to use for all system operations.
|
||||||
|
*/
|
||||||
|
public View(Controller controller) {
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates a complete repair session using hard-coded inputs. Demonstrates
|
||||||
|
* both the happy path and a selection of error scenarios.
|
||||||
|
*/
|
||||||
|
public void runFakeExecution() {
|
||||||
|
System.out.println("=== Repair Electric Bike System — Seminar 4 ===");
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Demo 1: Full happy-path run with loyalty discount
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
System.out.println("--- DEMO 1: Happy path with loyalty discount ---");
|
||||||
|
controller.startNewRepair();
|
||||||
|
System.out.println("[Mechanic] Started new repair session.");
|
||||||
|
|
||||||
|
lookupCustomer("0701234567");
|
||||||
|
|
||||||
|
enterBikeID("BIKE-001");
|
||||||
|
|
||||||
|
addRepairTask("Brake Pad Replacement");
|
||||||
|
addRepairTask("Tire Replacement");
|
||||||
|
|
||||||
|
String notes = "Front brake pads fully worn. Front tire flat (puncture). "
|
||||||
|
+ "Both parts replaced. Bike tested and approved.";
|
||||||
|
controller.enterDiagnosticReport(notes);
|
||||||
|
System.out.println("[Mechanic] Diagnostic report entered.");
|
||||||
|
|
||||||
|
RepairOrder repairOrder = controller.endRepair();
|
||||||
|
printRepairOrder(repairOrder);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Demo 2: Customer not found (alternative flow 5a)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("--- DEMO 2: Customer phone number not found (alt flow 5a) ---");
|
||||||
|
controller.startNewRepair();
|
||||||
|
lookupCustomer("0700000000"); // unknown phone number
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Demo 3: Bike not found
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("--- DEMO 3: Bike ID not found ---");
|
||||||
|
controller.startNewRepair();
|
||||||
|
enterBikeID("BIKE-999"); // unknown bike ID
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Demo 4: Task not found
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("--- DEMO 4: Repair task not found ---");
|
||||||
|
controller.startNewRepair();
|
||||||
|
enterBikeID("BIKE-002");
|
||||||
|
addRepairTask("Rocket Booster Installation"); // unknown task name
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Demo 5: Database failure (simulated)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("--- DEMO 5: Simulated database failure ---");
|
||||||
|
controller.startNewRepair();
|
||||||
|
enterBikeID("DB-FAIL-999"); // hardcoded failure trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Private helper methods — each wraps one controller call and handles
|
||||||
|
// all exceptions at the view level.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void lookupCustomer(String phoneNumber) {
|
||||||
|
try {
|
||||||
|
CustomerDTO customer = controller.lookupCustomer(phoneNumber);
|
||||||
|
System.out.println("[System] Customer found: " + customer.getName()
|
||||||
|
+ " (phone: " + customer.getPhoneNumber() + ")");
|
||||||
|
} catch (CustomerNotFoundException e) {
|
||||||
|
System.out.println("[System] ERROR: " + e.getMessage()
|
||||||
|
+ " — please check the phone number and try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enterBikeID(String bikeID) {
|
||||||
|
try {
|
||||||
|
BikeDTO bike = controller.enterBikeID(bikeID);
|
||||||
|
System.out.println("[System] Bike registered: ID=" + bike.getBikeID()
|
||||||
|
+ ", Owner=" + bike.getOwnerName());
|
||||||
|
} catch (BikeNotFoundException e) {
|
||||||
|
System.out.println("[System] ERROR: " + e.getMessage()
|
||||||
|
+ " — please enter a valid bike ID.");
|
||||||
|
} catch (DatabaseFailureException e) {
|
||||||
|
System.out.println("[System] ERROR: The system is temporarily unavailable. "
|
||||||
|
+ "Please try again later.");
|
||||||
|
ErrorLogger.logException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRepairTask(String taskName) {
|
||||||
|
try {
|
||||||
|
Amount runningTotal = controller.addRepairTask(taskName);
|
||||||
|
System.out.println("[System] Added task: \"" + taskName
|
||||||
|
+ "\" | Running total: " + runningTotal);
|
||||||
|
} catch (TaskNotFoundException e) {
|
||||||
|
System.out.println("[System] ERROR: " + e.getMessage()
|
||||||
|
+ " — please enter a valid task name.");
|
||||||
|
} catch (DatabaseFailureException e) {
|
||||||
|
System.out.println("[System] ERROR: The system is temporarily unavailable. "
|
||||||
|
+ "Please try again later.");
|
||||||
|
ErrorLogger.logException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printRepairOrder(RepairOrder repairOrder) {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println(" REPAIR ORDER ");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("Bike ID : " + repairOrder.getBike().getBikeID());
|
||||||
|
System.out.println("Owner : " + repairOrder.getBike().getOwnerName());
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
System.out.println("Repair Tasks:");
|
||||||
|
for (TaskDTO task : repairOrder.getTasks()) {
|
||||||
|
System.out.printf(" %-28s %s%n", task.getName(), task.getCost());
|
||||||
|
}
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
System.out.println("Total : " + repairOrder.getTotal());
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
System.out.println("Diagnostic Report:");
|
||||||
|
System.out.println(" " + repairOrder.getDiagnosticReport());
|
||||||
|
System.out.println("========================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
138
source/src/test/controller/ControllerTest.java
Normal file
138
source/src/test/controller/ControllerTest.java
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package controller;
|
||||||
|
|
||||||
|
import integration.BikeNotFoundException;
|
||||||
|
import integration.BikeRegistry;
|
||||||
|
import integration.CustomerNotFoundException;
|
||||||
|
import integration.CustomerRegistry;
|
||||||
|
import integration.DatabaseFailureException;
|
||||||
|
import integration.RepairTaskCatalog;
|
||||||
|
import integration.TaskNotFoundException;
|
||||||
|
import model.Amount;
|
||||||
|
import model.BikeDTO;
|
||||||
|
import model.CustomerDTO;
|
||||||
|
import model.RepairOrder;
|
||||||
|
import model.RepairShop;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ControllerTest {
|
||||||
|
|
||||||
|
private Controller controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
BikeRegistry bikeRegistry = BikeRegistry.getInstance();
|
||||||
|
RepairTaskCatalog taskCatalog = new RepairTaskCatalog();
|
||||||
|
CustomerRegistry customerRegistry = new CustomerRegistry();
|
||||||
|
RepairShop repairShop = new RepairShop();
|
||||||
|
controller = new Controller(repairShop, bikeRegistry, taskCatalog, customerRegistry);
|
||||||
|
controller.startNewRepair();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Customer lookup ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lookupCustomerWithKnownPhoneReturnsCorrectCustomer() throws CustomerNotFoundException {
|
||||||
|
CustomerDTO customer = controller.lookupCustomer("0701234567");
|
||||||
|
assertEquals("Alice Svensson", customer.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lookupCustomerWithUnknownPhoneThrowsCustomerNotFoundException() {
|
||||||
|
assertThrows(CustomerNotFoundException.class,
|
||||||
|
() -> controller.lookupCustomer("0700000000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bike ID entry ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enterBikeIdWithValidIdReturnsCorrectBikeId() throws BikeNotFoundException {
|
||||||
|
BikeDTO bike = controller.enterBikeID("BIKE-001");
|
||||||
|
assertEquals("BIKE-001", bike.getBikeID());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enterBikeIdWithUnknownIdThrowsBikeNotFoundException() {
|
||||||
|
assertThrows(BikeNotFoundException.class,
|
||||||
|
() -> controller.enterBikeID("BIKE-999"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enterBikeIdWithDatabaseFailureIdThrowsDatabaseFailureException() {
|
||||||
|
assertThrows(DatabaseFailureException.class,
|
||||||
|
() -> controller.enterBikeID(BikeRegistry.DB_FAILURE_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Repair tasks ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRepairTaskWithValidNameReturnsCorrectRunningTotal()
|
||||||
|
throws BikeNotFoundException, TaskNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-001");
|
||||||
|
Amount total = controller.addRepairTask("Brake Pad Replacement");
|
||||||
|
assertEquals(350.0, total.getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addTwoRepairTasksReturnsCumulativeTotal()
|
||||||
|
throws BikeNotFoundException, TaskNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-001");
|
||||||
|
controller.addRepairTask("Brake Pad Replacement");
|
||||||
|
Amount total = controller.addRepairTask("Tire Replacement");
|
||||||
|
assertEquals(850.0, total.getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRepairTaskWithUnknownNameThrowsTaskNotFoundException()
|
||||||
|
throws BikeNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-001");
|
||||||
|
assertThrows(TaskNotFoundException.class,
|
||||||
|
() -> controller.addRepairTask("Non-existing Task"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- End repair ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairReturnsNonNullRepairOrder()
|
||||||
|
throws BikeNotFoundException, TaskNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-001");
|
||||||
|
controller.addRepairTask("Battery Check");
|
||||||
|
RepairOrder repairOrder = controller.endRepair();
|
||||||
|
assertNotNull(repairOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairReturnsRepairOrderWithCorrectBikeId() throws BikeNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-002");
|
||||||
|
RepairOrder repairOrder = controller.endRepair();
|
||||||
|
assertEquals("BIKE-002", repairOrder.getBike().getBikeID());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enterDiagnosticReportIsReflectedInFinalRepairOrder()
|
||||||
|
throws BikeNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-003");
|
||||||
|
controller.enterDiagnosticReport("Chain replaced and tested.");
|
||||||
|
RepairOrder repairOrder = controller.endRepair();
|
||||||
|
assertEquals("Chain replaced and tested.", repairOrder.getDiagnosticReport());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- State preservation on exception ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addTaskAfterFailedAddTaskStillUsesCorrectRunningTotal()
|
||||||
|
throws BikeNotFoundException, TaskNotFoundException {
|
||||||
|
controller.enterBikeID("BIKE-001");
|
||||||
|
controller.addRepairTask("Brake Pad Replacement"); // +350
|
||||||
|
|
||||||
|
// Failed task — state must not change
|
||||||
|
assertThrows(TaskNotFoundException.class,
|
||||||
|
() -> controller.addRepairTask("Bad Task Name"));
|
||||||
|
|
||||||
|
// Next valid task should build on the previous total
|
||||||
|
Amount total = controller.addRepairTask("Battery Check"); // +200
|
||||||
|
assertEquals(550.0, total.getValue(), 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
source/src/test/integration/BikeRegistryTest.java
Normal file
76
source/src/test/integration/BikeRegistryTest.java
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
import model.BikeDTO;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class BikeRegistryTest {
|
||||||
|
|
||||||
|
// BikeRegistry is a Singleton — obtain the shared instance in each test.
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findBikeWithExistingIdReturnsBikeDTO() throws BikeNotFoundException {
|
||||||
|
BikeDTO bike = BikeRegistry.getInstance().findBike("BIKE-001");
|
||||||
|
assertNotNull(bike);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findBikeWithExistingIdReturnsCorrectBikeId() throws BikeNotFoundException {
|
||||||
|
BikeDTO bike = BikeRegistry.getInstance().findBike("BIKE-001");
|
||||||
|
assertEquals("BIKE-001", bike.getBikeID());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findBikeWithExistingIdReturnsCorrectOwnerName() throws BikeNotFoundException {
|
||||||
|
BikeDTO bike = BikeRegistry.getInstance().findBike("BIKE-002");
|
||||||
|
assertEquals("Bob Lindqvist", bike.getOwnerName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findBikeWithAllThreeSampleBikesReturnsNonNull() throws BikeNotFoundException {
|
||||||
|
assertNotNull(BikeRegistry.getInstance().findBike("BIKE-001"));
|
||||||
|
assertNotNull(BikeRegistry.getInstance().findBike("BIKE-002"));
|
||||||
|
assertNotNull(BikeRegistry.getInstance().findBike("BIKE-003"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findBikeWithUnknownIdThrowsBikeNotFoundException() {
|
||||||
|
assertThrows(BikeNotFoundException.class,
|
||||||
|
() -> BikeRegistry.getInstance().findBike("BIKE-999"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownBikeNotFoundExceptionContainsCorrectBikeId() {
|
||||||
|
BikeNotFoundException ex = assertThrows(BikeNotFoundException.class,
|
||||||
|
() -> BikeRegistry.getInstance().findBike("BIKE-999"));
|
||||||
|
assertEquals("BIKE-999", ex.getBikeID());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownBikeNotFoundExceptionMessageMentionsBikeId() {
|
||||||
|
BikeNotFoundException ex = assertThrows(BikeNotFoundException.class,
|
||||||
|
() -> BikeRegistry.getInstance().findBike("BIKE-999"));
|
||||||
|
assertTrue(ex.getMessage().contains("BIKE-999"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findBikeWithDatabaseFailureIdThrowsDatabaseFailureException() {
|
||||||
|
assertThrows(DatabaseFailureException.class,
|
||||||
|
() -> BikeRegistry.getInstance().findBike(BikeRegistry.DB_FAILURE_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownDatabaseFailureExceptionContainsOperationName() {
|
||||||
|
DatabaseFailureException ex = assertThrows(DatabaseFailureException.class,
|
||||||
|
() -> BikeRegistry.getInstance().findBike(BikeRegistry.DB_FAILURE_ID));
|
||||||
|
assertTrue(ex.getMessage().contains("BikeRegistry.findBike"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getInstanceReturnsSameInstanceOnMultipleCalls() {
|
||||||
|
BikeRegistry first = BikeRegistry.getInstance();
|
||||||
|
BikeRegistry second = BikeRegistry.getInstance();
|
||||||
|
assertSame(first, second);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
source/src/test/integration/CustomerRegistryTest.java
Normal file
56
source/src/test/integration/CustomerRegistryTest.java
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
import model.CustomerDTO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class CustomerRegistryTest {
|
||||||
|
|
||||||
|
private CustomerRegistry customerRegistry;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
customerRegistry = new CustomerRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findCustomerWithKnownPhoneReturnsCorrectCustomer() throws CustomerNotFoundException {
|
||||||
|
CustomerDTO customer = customerRegistry.findCustomer("0701234567");
|
||||||
|
assertEquals("Alice Svensson", customer.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findCustomerWithKnownPhoneReturnsCorrectPhoneNumber() throws CustomerNotFoundException {
|
||||||
|
CustomerDTO customer = customerRegistry.findCustomer("0701234567");
|
||||||
|
assertEquals("0701234567", customer.getPhoneNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findCustomerWithAllThreeSamplePhonesReturnsNonNull() throws CustomerNotFoundException {
|
||||||
|
assertNotNull(customerRegistry.findCustomer("0701234567"));
|
||||||
|
assertNotNull(customerRegistry.findCustomer("0709876543"));
|
||||||
|
assertNotNull(customerRegistry.findCustomer("0705551234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findCustomerWithUnknownPhoneThrowsCustomerNotFoundException() {
|
||||||
|
assertThrows(CustomerNotFoundException.class,
|
||||||
|
() -> customerRegistry.findCustomer("0700000000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownCustomerNotFoundExceptionContainsCorrectPhoneNumber() {
|
||||||
|
CustomerNotFoundException ex = assertThrows(CustomerNotFoundException.class,
|
||||||
|
() -> customerRegistry.findCustomer("0799999999"));
|
||||||
|
assertEquals("0799999999", ex.getPhoneNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownCustomerNotFoundExceptionMessageMentionsPhoneNumber() {
|
||||||
|
CustomerNotFoundException ex = assertThrows(CustomerNotFoundException.class,
|
||||||
|
() -> customerRegistry.findCustomer("0799999999"));
|
||||||
|
assertTrue(ex.getMessage().contains("0799999999"));
|
||||||
|
}
|
||||||
|
}
|
||||||
58
source/src/test/integration/RepairTaskCatalogTest.java
Normal file
58
source/src/test/integration/RepairTaskCatalogTest.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package integration;
|
||||||
|
|
||||||
|
import model.Amount;
|
||||||
|
import model.TaskDTO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RepairTaskCatalogTest {
|
||||||
|
|
||||||
|
private RepairTaskCatalog catalog;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
catalog = new RepairTaskCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findTaskWithExistingNameReturnsTaskDTO() throws TaskNotFoundException {
|
||||||
|
TaskDTO task = catalog.findTask("Brake Pad Replacement");
|
||||||
|
assertNotNull(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findTaskWithExistingNameReturnsCorrectCost() throws TaskNotFoundException {
|
||||||
|
TaskDTO task = catalog.findTask("Battery Check");
|
||||||
|
assertEquals(200.0, task.getCost().getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findTaskWithAllFourSampleTasksReturnsNonNull() throws TaskNotFoundException {
|
||||||
|
assertNotNull(catalog.findTask("Brake Pad Replacement"));
|
||||||
|
assertNotNull(catalog.findTask("Tire Replacement"));
|
||||||
|
assertNotNull(catalog.findTask("Battery Check"));
|
||||||
|
assertNotNull(catalog.findTask("Chain Lubrication"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findTaskWithUnknownNameThrowsTaskNotFoundException() {
|
||||||
|
assertThrows(TaskNotFoundException.class,
|
||||||
|
() -> catalog.findTask("Rocket Booster Installation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownTaskNotFoundExceptionContainsCorrectTaskName() {
|
||||||
|
TaskNotFoundException ex = assertThrows(TaskNotFoundException.class,
|
||||||
|
() -> catalog.findTask("Unknown Task"));
|
||||||
|
assertEquals("Unknown Task", ex.getTaskName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thrownTaskNotFoundExceptionMessageMentionsTaskName() {
|
||||||
|
TaskNotFoundException ex = assertThrows(TaskNotFoundException.class,
|
||||||
|
() -> catalog.findTask("Unknown Task"));
|
||||||
|
assertTrue(ex.getMessage().contains("Unknown Task"));
|
||||||
|
}
|
||||||
|
}
|
||||||
106
source/src/test/model/ActiveRepairTest.java
Normal file
106
source/src/test/model/ActiveRepairTest.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ActiveRepairTest {
|
||||||
|
|
||||||
|
private ActiveRepair activeRepair;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
activeRepair = new ActiveRepair();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addSingleTaskReturnsTaskCostAsRunningTotal() {
|
||||||
|
TaskDTO task = new TaskDTO("Brake Pad Replacement", new Amount(350));
|
||||||
|
Amount runningTotal = activeRepair.addTask(task);
|
||||||
|
assertEquals(350, runningTotal.getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addTwoTasksReturnsCorrectCumulativeTotal() {
|
||||||
|
activeRepair.addTask(new TaskDTO("Brake Pad Replacement", new Amount(350)));
|
||||||
|
Amount runningTotal = activeRepair.addTask(new TaskDTO("Tire Replacement", new Amount(500)));
|
||||||
|
assertEquals(850, runningTotal.getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairWithRegisteredBikeReturnsBikeInRepairOrder() {
|
||||||
|
BikeDTO bike = new BikeDTO("BIKE-001", "Alice Svensson");
|
||||||
|
activeRepair.registerBike(bike);
|
||||||
|
RepairOrder repairOrder = activeRepair.endRepair();
|
||||||
|
assertEquals("BIKE-001", repairOrder.getBike().getBikeID());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairAfterAddingTasksReturnsAllTasksInOrder() {
|
||||||
|
activeRepair.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
activeRepair.addTask(new TaskDTO("Brake Pad Replacement", new Amount(350)));
|
||||||
|
activeRepair.addTask(new TaskDTO("Tire Replacement", new Amount(500)));
|
||||||
|
RepairOrder repairOrder = activeRepair.endRepair();
|
||||||
|
assertEquals(2, repairOrder.getTasks().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairWithNoDiscountReturnsUnchangedTotal() {
|
||||||
|
activeRepair.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
activeRepair.addTask(new TaskDTO("Brake Pad Replacement", new Amount(350)));
|
||||||
|
RepairOrder repairOrder = activeRepair.endRepair();
|
||||||
|
assertEquals(350, repairOrder.getTotal().getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairWithLoyaltyDiscountReducesTotalByTenPercent() {
|
||||||
|
activeRepair.setDiscountStrategy(new LoyaltyDiscount());
|
||||||
|
activeRepair.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
activeRepair.addTask(new TaskDTO("Tire Replacement", new Amount(1000)));
|
||||||
|
RepairOrder repairOrder = activeRepair.endRepair();
|
||||||
|
assertEquals(900, repairOrder.getTotal().getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endRepairWithoutTasksReturnsTotalOfZero() {
|
||||||
|
activeRepair.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
RepairOrder repairOrder = activeRepair.endRepair();
|
||||||
|
assertEquals(0, repairOrder.getTotal().getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enterDiagnosticReportIsIncludedInRepairOrder() {
|
||||||
|
activeRepair.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
String report = "Front tire replaced.";
|
||||||
|
activeRepair.enterDiagnosticReport(report);
|
||||||
|
RepairOrder repairOrder = activeRepair.endRepair();
|
||||||
|
assertEquals(report, repairOrder.getDiagnosticReport());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observerIsNotifiedWhenBikeIsRegistered() {
|
||||||
|
boolean[] notified = {false};
|
||||||
|
activeRepair.addObserver(order -> notified[0] = true);
|
||||||
|
activeRepair.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
assertTrue(notified[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observerIsNotifiedWhenTaskIsAdded() {
|
||||||
|
boolean[] notified = {false};
|
||||||
|
activeRepair.addObserver(order -> notified[0] = true);
|
||||||
|
activeRepair.addTask(new TaskDTO("Battery Check", new Amount(200)));
|
||||||
|
assertTrue(notified[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observerReceivesCorrectBikeWhenNotified() {
|
||||||
|
BikeDTO[] received = {null};
|
||||||
|
activeRepair.addObserver(order -> received[0] = order.getBike());
|
||||||
|
BikeDTO bike = new BikeDTO("BIKE-001", "Alice Svensson");
|
||||||
|
activeRepair.registerBike(bike);
|
||||||
|
assertNotNull(received[0]);
|
||||||
|
assertEquals("BIKE-001", received[0].getBikeID());
|
||||||
|
}
|
||||||
|
}
|
||||||
41
source/src/test/model/AmountTest.java
Normal file
41
source/src/test/model/AmountTest.java
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class AmountTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addReturnsCorrectSum() {
|
||||||
|
Amount a = new Amount(300);
|
||||||
|
Amount b = new Amount(50);
|
||||||
|
assertEquals(350.0, a.add(b).getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subtractReturnsCorrectDifference() {
|
||||||
|
Amount a = new Amount(500);
|
||||||
|
Amount b = new Amount(200);
|
||||||
|
assertEquals(300.0, a.subtract(b).getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiplyByOneReturnsUnchangedValue() {
|
||||||
|
Amount a = new Amount(400);
|
||||||
|
assertEquals(400.0, a.multiply(1.0).getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiplyByNinetyPercentReturnsDiscountedValue() {
|
||||||
|
Amount a = new Amount(1000);
|
||||||
|
assertEquals(900.0, a.multiply(0.9).getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringFormatsValueWithTwoDecimalPlaces() {
|
||||||
|
Amount a = new Amount(123.456);
|
||||||
|
assertTrue(a.toString().startsWith("123.46"));
|
||||||
|
}
|
||||||
|
}
|
||||||
50
source/src/test/model/RepairShopTest.java
Normal file
50
source/src/test/model/RepairShopTest.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package model;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RepairShopTest {
|
||||||
|
|
||||||
|
private RepairShop repairShop;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repairShop = new RepairShop();
|
||||||
|
repairShop.startRepair();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registerBikeAndEndRepairReturnsBikeInOrder() {
|
||||||
|
repairShop.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
RepairOrder order = repairShop.endRepair();
|
||||||
|
assertEquals("BIKE-001", order.getBike().getBikeID());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addTaskReturnsCorrectRunningTotal() {
|
||||||
|
repairShop.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
Amount total = repairShop.addTask(new TaskDTO("Brake Pad Replacement", new Amount(350)));
|
||||||
|
assertEquals(350, total.getValue(), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void observerRegisteredOnShopIsNotifiedOnRepairUpdate() {
|
||||||
|
boolean[] notified = {false};
|
||||||
|
repairShop.addObserver(order -> notified[0] = true);
|
||||||
|
repairShop.startRepair(); // new session picks up the observer
|
||||||
|
repairShop.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
assertTrue(notified[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void discountStrategySetOnShopIsAppliedToEndRepair() {
|
||||||
|
repairShop.setDiscountStrategy(new LoyaltyDiscount());
|
||||||
|
repairShop.startRepair();
|
||||||
|
repairShop.registerBike(new BikeDTO("BIKE-001", "Alice Svensson"));
|
||||||
|
repairShop.addTask(new TaskDTO("Tire Replacement", new Amount(1000)));
|
||||||
|
RepairOrder order = repairShop.endRepair();
|
||||||
|
assertEquals(900, order.getTotal().getValue(), 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
source/src/test/util/ErrorLoggerTest.java
Normal file
58
source/src/test/util/ErrorLoggerTest.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for ErrorLogger. Because ErrorLogger uses a static FileLogger that opens
|
||||||
|
* the log file once on class load, these tests append to whatever state the file
|
||||||
|
* is already in and clean up only at the very end.
|
||||||
|
*/
|
||||||
|
class ErrorLoggerTest {
|
||||||
|
|
||||||
|
private static final String ERROR_LOG_FILE = "error.log";
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void cleanUp() throws IOException {
|
||||||
|
Files.deleteIfExists(Paths.get(ERROR_LOG_FILE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logExceptionDoesNotThrow() {
|
||||||
|
// Smoke test: logException must not propagate any exception.
|
||||||
|
assertDoesNotThrow(() -> ErrorLogger.logException(
|
||||||
|
new RuntimeException("smoke test error")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logExceptionCreatesLogFile() {
|
||||||
|
ErrorLogger.logException(new RuntimeException("file creation test"));
|
||||||
|
assertTrue(Files.exists(Paths.get(ERROR_LOG_FILE)),
|
||||||
|
"error.log should exist after logException is called");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logExceptionWritesExceptionMessageToFile() throws IOException {
|
||||||
|
String uniqueMsg = "unique-db-failure-test-" + System.nanoTime();
|
||||||
|
ErrorLogger.logException(new RuntimeException(uniqueMsg));
|
||||||
|
String content = Files.readString(Paths.get(ERROR_LOG_FILE));
|
||||||
|
assertTrue(content.contains(uniqueMsg),
|
||||||
|
"Log file should contain the exception message");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logExceptionWritesStackTraceToFile() throws IOException {
|
||||||
|
ErrorLogger.logException(new RuntimeException("stack trace check"));
|
||||||
|
String content = Files.readString(Paths.get(ERROR_LOG_FILE));
|
||||||
|
assertTrue(content.contains("RuntimeException"),
|
||||||
|
"Log file should contain the exception class name in the stack trace");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
56
source/src/test/util/FileLoggerTest.java
Normal file
56
source/src/test/util/FileLoggerTest.java
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class FileLoggerTest {
|
||||||
|
|
||||||
|
private static final String TEST_LOG_FILE = "test-file-logger.log";
|
||||||
|
private Path logPath;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws IOException {
|
||||||
|
logPath = Paths.get(TEST_LOG_FILE);
|
||||||
|
Files.deleteIfExists(logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws IOException {
|
||||||
|
Files.deleteIfExists(logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logCreatesFileIfItDoesNotExist() {
|
||||||
|
FileLogger logger = new FileLogger(TEST_LOG_FILE);
|
||||||
|
logger.log("hello");
|
||||||
|
assertTrue(Files.exists(logPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logWritesMessageToFile() throws IOException {
|
||||||
|
FileLogger logger = new FileLogger(TEST_LOG_FILE);
|
||||||
|
logger.log("test message");
|
||||||
|
List<String> lines = Files.readAllLines(logPath);
|
||||||
|
assertFalse(lines.isEmpty());
|
||||||
|
assertTrue(lines.get(0).contains("test message"),
|
||||||
|
"Log file should contain the logged message");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logAppendsMultipleMessages() throws IOException {
|
||||||
|
FileLogger logger = new FileLogger(TEST_LOG_FILE);
|
||||||
|
logger.log("first");
|
||||||
|
logger.log("second");
|
||||||
|
List<String> lines = Files.readAllLines(logPath);
|
||||||
|
assertEquals(2, lines.size(), "Two messages should produce two lines");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user