diff --git a/.gitignore b/.gitignore
index 6ac465db..244268f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
target/
/.idea/
+.env
diff --git a/mvnw b/mvnw
old mode 100644
new mode 100755
diff --git a/pom.xml b/pom.xml
index c40f667e..f2b3aecd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,16 @@
javafx-controls
${javafx.version}
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.19.0
+
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
org.openjfx
javafx-fxml
@@ -63,6 +73,15 @@
true
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 25
+ 25
+ --enable-preview
+
+
diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java
index fdd160a0..8ea5159c 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,213 @@
package com.example;
+import javafx.beans.binding.Bindings;
+
import javafx.fxml.FXML;
-import javafx.scene.control.Label;
+
+import javafx.scene.control.*;
+
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+
+import java.io.File;
+
+
/**
- * Controller layer: mediates between the view (FXML) and the model.
+ * Controller layer: Manages the user interface and mediates communication between the View (FXML) and the Model (HelloModel).
+ * Handles user interactions such as sending messages, attaching files, and displaying chat content.
*/
public class HelloController {
- private final HelloModel model = new HelloModel();
+ private static final String HOST_NAME = System.getenv("HOST_NAME");
+
+ static {
+ if (HOST_NAME == null || HOST_NAME.isBlank()) {
+ throw new IllegalStateException(
+ "Environment variable HOST_NAME must be set to the server URL."
+ );
+ }
+ }
+
+
+
+
+
+ /**
+ * The model instance that holds application data and business logic.
+ * Initializes connection to the specified Ntfy server.
+ */
+ private final HelloModel model = new HelloModel(new NtfyConnectionImpl(HOST_NAME));
+
+
+ /**
+ * The ListView component displaying the list of chat messages.
+ */
@FXML
- private Label messageLabel;
+ public ListView chatListView;
+ /**
+ * The label displaying the current fixed topic being used.
+ */
+ @FXML
+ public Label topicLabel;
+
+ /**
+ * The label indicating the name of the file currently attached for sending.
+ */
+ @FXML
+ public Label attachedFileLabel;
+
+
+ /**
+ * The button used to send the message or the attached file.
+ */
+ @FXML
+ private Button sendButton;
+
+ @FXML
+ private TextField messageInput;
+
+ @FXML
+ private Button attachFile;
+
+
+ /**
+ * Initializes the controller. This method is called automatically by the FXML loader.
+ * It sets up bindings between the view components and the model and configures the chat list view.
+ */
@FXML
private void initialize() {
- if (messageLabel != null) {
- messageLabel.setText(model.getGreeting());
+ if (sendButton != null) {
+ sendButton.setText(model.getGreeting());
+
+ // Bindning: Knappen är inaktiverad ENDAST om BÅDE meddelandet är tomt OCH fil inte är bifogad
+ sendButton.disableProperty().bind(
+ Bindings.createBooleanBinding(() -> {
+ boolean isMessageEmpty = model.messageToSendProperty().get() == null ||
+ model.messageToSendProperty().get().trim().isEmpty();
+ boolean isFileNotAttached = model.fileToSendProperty().get() == null;
+
+ return isMessageEmpty && isFileNotAttached;
+ },
+ model.messageToSendProperty(),
+ model.fileToSendProperty())
+ );
+ }
+
+ if (topicLabel != null) {
+ // Visar den fasta topicen
+ topicLabel.textProperty().bind(
+ Bindings.concat("Fixed Topic: ", model.currentTopicProperty())
+ );
+ }
+
+ // Hanterar visning av bifogad fil
+ if (attachedFileLabel != null) {
+ model.fileToSendProperty().addListener((obs, oldFile, newFile) -> {
+ if (newFile != null) {
+ attachedFileLabel.setText("Attached file: " + newFile.getName());
+ attachedFileLabel.setStyle("-fx-font-style: italic;" +
+ " -fx-font-size: 12px;" +
+ " -fx-text-fill: #008000;");
+ } else {
+ attachedFileLabel.setText("No file attached");
+ attachedFileLabel.setStyle("-fx-font-style: italic; " +
+ "-fx-font-size: 12px; " +
+ "-fx-text-fill: #333;");
+ }
+ });
+ attachedFileLabel.setText("No file attached");
+ }
+
+ if (messageInput!=null){
+ messageInput.textProperty().bindBidirectional(model.messageToSendProperty());
+ }
+
+ if(chatListView!=null){
+ chatListView.setItems(model.getMessages());
+ // Använd den enkla CellFactoryn
+ chatListView.setCellFactory(param -> new SimpleMessageCell());
+ }
+ }
+
+ /**
+ * A simple custom ListCell implementation for the chatListView.
+ * It displays the message text or a placeholder for file uploads, and indicates if the message was sent locally.
+ */
+ private static class SimpleMessageCell extends ListCell {
+
+ @Override
+ protected void updateItem(NtfyMessageDto item, boolean empty) {
+ super.updateItem(item, empty);
+
+ if (empty || item == null) {
+ setText(null);
+ setGraphic(null);
+ setStyle(null);
+ } else {
+ // Hämta meddelandet eller visa filstatus om meddelandet är tomt
+ String displayMessage = item.message() != null && !item.message().trim().isEmpty()
+ ? item.message()
+ : ("file".equals(item.event())) ? item.topic() + " Uploaded" : "";
+
+ // Lägg till prefix för att visa om det är skickat lokalt
+ String prefix = item.isLocal() ? "(Sent) " : "";
+
+ setText(prefix + displayMessage);
+ setGraphic(null);
+
+ // Mycket enkel stil utan bubblor/färger. Använd standard utseende.
+ setStyle(null);
+ }
+ }
+ }
+
+
+ /**
+ * Handles the action of the send button.
+ * If a file is attached, it calls the model to send the file; otherwise, it calls the model to send the text message.
+ */
+ @FXML
+ protected void sendMessage() {
+ if (model.fileToSendProperty().get() != null) {
+ // Om en fil är bifogad, skicka filen och rensa bilagan i modellen
+ model.sendFile();
+ } else {
+ // Annars, skicka textmeddelandet
+ model.sendMessage();
+ }
+
+ if (messageInput!=null){
+ messageInput.requestFocus();
+ }
+ }
+
+ /**
+ * Handles the action of the attach file button.
+ * Opens a FileChooser dialog and sets the selected file in the model.
+ */
+ @FXML
+ protected void attachFile() {
+ // Hämta scenen från en av kontrollerna
+
+ if (attachFile == null || attachFile.getScene() == null || attachFile.getScene().getWindow() == null) {
+ System.err.println("Cannot open file chooser: UI not ready.");
+ return;
+ }
+ Stage stage = (Stage) attachFile.getScene().getWindow();
+
+
+
+
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Choose file to attach");
+
+ File selectedFile = fileChooser.showOpenDialog(stage);
+
+ if (selectedFile != null) {
+ model.setFileToSend(selectedFile);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java
index 96bdc5ca..cfc280e1 100644
--- a/src/main/java/com/example/HelloFX.java
+++ b/src/main/java/com/example/HelloFX.java
@@ -1,5 +1,6 @@
package com.example;
+
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
@@ -10,6 +11,9 @@ public class HelloFX extends Application {
@Override
public void start(Stage stage) throws Exception {
+
+
+
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java
index 385cfd10..23cf46ee 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,173 @@
package com.example;
+
+import javafx.application.Platform;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+
+import javafx.collections.ObservableList;
+
+import java.io.File;
+
+import java.util.concurrent.CompletableFuture;
+
+
/**
* Model layer: encapsulates application data and business logic.
+ * Manages the current message, topic, list of received messages, and connection to Ntfy.
*/
public class HelloModel {
+ private final NtfyConnection connection;
+ private final StringProperty messageToSend = new SimpleStringProperty("");
+ private final StringProperty currentTopic = new SimpleStringProperty("mytopic");
+ private final ObservableList messages = FXCollections.observableArrayList();
+ private final ObjectProperty fileToSend = new SimpleObjectProperty<>(null); // Ny egenskap för filbilaga
+
/**
- * Returns a greeting based on the current Java and JavaFX versions.
+ * Initializes the model and establishes connection to the specified Ntfy server.
+ * @param connection The Ntfy connection implementation to use (e.g., NtfyConnectionImpl or a Spy).
+ */
+ public HelloModel(NtfyConnection connection) {
+ this.connection = connection;
+ connection.connect(currentTopic.get(), this::receiveMessage);
+ }
+ /**
+ * Returns a standard greeting string.
+ * @return The greeting string.
*/
public String getGreeting() {
- String javaVersion = System.getProperty("java.version");
- String javafxVersion = System.getProperty("javafx.version");
- return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
+ return "Skicka meddelande";
+ }
+
+ // NY METOD: Hanterar att köra koden på JavaFX-tråden ELLER direkt i testmiljö
+ private static void runOnFx(Runnable task) {
+ try {
+ if (Platform.isFxApplicationThread()) task.run();
+ else Platform.runLater(task);
+ } catch (IllegalStateException notInitialized) {
+ task.run();
+ }
+ }
+
+ /**
+ * Sends the current text message asynchronously.
+ * The network call is moved off the FX thread. UI updates happen via runOnFx.
+ */
+ public void sendMessage() {
+ final String message = messageToSend.get();
+ if (message == null || message.trim().isEmpty()) {
+ System.err.println("Cannot send empty message.");
+ return;
+ }
+
+ // 1. Markera meddelandet som lokalt skickat och lägg till i listan (UI-uppdatering)
+ final NtfyMessageDto localSentDto = new NtfyMessageDto(message, currentTopic.get(), "text", true);
+ runOnFx(() -> {
+ messages.add(localSentDto);
+ messageToSend.set(""); // Rensa inmatningsfältet
+ });
+
+ // 2. Utför nätverksanropet på en bakgrundstråd
+ CompletableFuture.runAsync(() -> {
+ // Nätverksanropet sker här i bakgrunden
+ try {
+ connection.send(message, currentTopic.get());
+ } catch (Exception e) {
+ System.err.println("Failed to send message: " + e.getMessage());
+
+ }
+ });
+ }
+
+ /**
+ * Sends the currently attached file to the Ntfy server.
+ * The file's name and a temporary message are added to the local list before sending.
+ * The file attachment is cleared after sending.
+ */
+ public void sendFile() {
+ File file = fileToSend.get();
+ if (file == null) {
+ System.err.println("Cannot send file: no file selected.");
+ return;
+ }
+ final NtfyMessageDto localSentDto = new NtfyMessageDto(file.getName(), currentTopic.get(), "file", true);
+ runOnFx(() -> {
+ messages.add(localSentDto);
+ fileToSend.set(null); // Rensa filbilagan i UI
+ });
+
+
+ // 2. Utför nätverksanropet på en bakgrundstråd
+ CompletableFuture.runAsync(() -> {
+ // Nätverksanropet sker här i bakgrunden
+ try {
+ connection.sendFile(file, currentTopic.get());
+ } catch( Exception e){
+ System.err.println("Failed to send file: " + e.getMessage());
+ }
+ });
+ }
+
+ /**
+ * Property for the file currently attached to be sent.
+ * @return The ObjectProperty containing the File object, or null if no file is attached.
+ */
+ public ObjectProperty fileToSendProperty() {
+ return fileToSend;
+ }
+
+ /**
+ * Sets the file to be sent with the next message.
+ * @param file The file to attach. Set to null to clear the attachment.
+ */
+ public void setFileToSend(File file) {
+ this.fileToSend.set(file);
+ }
+
+
+ /**
+ * Handles an incoming message from the Ntfy connection and adds it to the message list on the FX thread.
+ * @param message The received NtfyMessageDto.
+ */
+ private void receiveMessage(NtfyMessageDto message) {
+
+ runOnFx(() -> messages.add(message));
+ }
+
+ /**
+ * Returns the observable list of messages received and sent.
+ * @return The ObservableList of NtfyMessageDto objects.
+ */
+ public ObservableList getMessages() {
+ return messages;
+ }
+
+ /**
+ * Property for the message currently being composed to send.
+ * @return The StringProperty holding the message content.
+ */
+ public StringProperty messageToSendProperty() {
+ return messageToSend;
+ }
+
+ /**
+ * Property for the current Ntfy topic being subscribed to.
+ * @return The StringProperty holding the current topic name.
+ */
+ public StringProperty currentTopicProperty() {
+ return currentTopic;
+ }
+
+
+ /**
+ * Sets the message content to send. Used by the controller for bidirectional binding.
+ * @param message The new message content.
+ */
+ public void setMessageToSend(String message) {
+ this.messageToSend.set(message);
}
}
diff --git a/src/main/java/com/example/NtfyConnection.java b/src/main/java/com/example/NtfyConnection.java
new file mode 100644
index 00000000..1aa42e92
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,22 @@
+package com.example;
+
+import java.io.File;
+
+import java.util.function.Consumer;
+
+public interface NtfyConnection {
+
+ String getTopic();
+
+ boolean send(String message, String topic);
+
+ boolean sendFile(File file, String topic);
+
+ void connect(String topic, Consumer messageHandler);
+
+
+
+ public void receive(Consumer messageHandler);
+
+
+}
diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java
new file mode 100644
index 00000000..8c5cfd2b
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,175 @@
+package com.example;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.cdimascio.dotenv.Dotenv;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Files;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+
+/**
+ * Implementation of NtfyConnection using Java's built-in HttpClient for both sending messages and subscribing to a topic.
+ * Manages the HTTP connection logic for text messages, file uploads.
+ */
+public class NtfyConnectionImpl implements NtfyConnection {
+
+ private final HttpClient http = HttpClient.newHttpClient();
+ private final String hostName;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+
+ private String currentTopic = "mytopic";
+
+ private CompletableFuture currentSubscription = CompletableFuture.completedFuture(null);
+
+
+ public NtfyConnectionImpl() {
+
+ Dotenv dotenv = Dotenv.load();
+
+ this.hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
+ }
+
+ /**
+ * Creates a new connection implementation.
+ * hostName The base URL of the Ntfy server.
+ */
+ public NtfyConnectionImpl(String hostName) {
+ this.hostName = hostName;
+ }
+
+ @Override
+ public String getTopic() {
+ return currentTopic;
+ }
+
+
+ /**
+ * Establishes a connection to the Ntfy topic to receive messages in real-time.
+ * This method runs asynchronously in a dedicated thread.
+ */
+ @Override
+ public void connect(String newTopic, Consumer messageHandler) {
+
+ currentSubscription.cancel(true);
+ System.out.println("Cancelled subscription on " + currentTopic);
+
+
+ currentTopic = newTopic;
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/" + currentTopic + "/json"))
+ .build();
+
+
+ currentSubscription = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+ .thenAccept(response -> response.body()
+ .map(s -> {
+ try {
+ return mapper.readValue(s, NtfyMessageDto.class);
+ } catch (JsonProcessingException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .filter(message -> message.event().equals("message"))
+ .forEach(messageHandler))
+ .exceptionally(e -> {
+
+ if (!(e instanceof java.util.concurrent.CancellationException)) {
+ System.out.println("Failure in receiving on: " + currentTopic + ": " + e.getMessage());
+ }
+ return null;
+ });
+ }
+
+
+ /**
+ * Sends a text message to the specified Ntfy topic.
+ * @param message The text content of the message.
+ * @param topic The Ntfy topic to send the message to.
+ */
+ @Override
+ public boolean send(String message, String topic) {
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(message))
+ .header("Cache", "no")
+ .uri(URI.create(hostName + "/" + topic))
+ .build();
+
+ try {
+
+ HttpResponse response = http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
+
+ return response.statusCode() >= 200 && response.statusCode() < 300;
+ } catch (IOException e) {
+ System.out.println("Error sending message to " + topic + ": " + e.getMessage());
+ } catch (InterruptedException e) {
+ System.out.println("Interrupted sending message to " + topic + ": " + e.getMessage());
+ Thread.currentThread().interrupt();
+ }
+ return false;
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ // Återanvänder connect-logiken
+ connect(currentTopic, messageHandler);
+ }
+
+
+ private String cleanHeaderValue(String value) {
+
+ return value.replaceAll("[^\\w.\\-]", "-");
+ }
+
+
+ @Override
+ public boolean sendFile(File file, String topic) {
+ if (!file.exists() || !file.isFile()) {
+ System.out.println("File not valid: " + file.getAbsolutePath());
+ return false;
+ }
+
+ try {
+
+ String contentType = Files.probeContentType(file.toPath());
+ if (contentType == null) {
+ contentType = "application/octet-stream";
+ }
+
+
+ String cleanedFilename = cleanHeaderValue(file.getName());
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofFile(file.toPath()))
+ .header("Content-Type", contentType)
+ .header("Filename", cleanedFilename) // Använder det rensade filnamnet
+ .uri(URI.create(hostName + "/" + topic))
+ .build();
+
+
+ HttpResponse response = http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
+ return response.statusCode() >= 200 && response.statusCode() < 300;
+
+ } catch (IOException e) {
+ System.out.println("Could not transfer file or read file: " + e.getMessage());
+ return false;
+ } catch (InterruptedException e) {
+ System.out.println("Interrupted file transfer: " + e.getMessage());
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java
new file mode 100644
index 00000000..592300be
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,36 @@
+package com.example;
+
+
+
+
+public record NtfyMessageDto(String id, long time, String event, String topic, String type, String message, boolean isLocal) {
+
+ // Sekundär konstruktor för mottagna meddelanden (isLocal = false)
+ public NtfyMessageDto(String id, long time, String event, String topic, String type, String message) {
+ this(id, time, event, topic, type, message, false);
+ }
+
+ /**
+ * Konstruktor för att skapa ett lokalt skickat meddelande (som visas i listan innan bekräftelse).
+ * @param message Den huvudsakliga meddelandetexten eller filnamnet.
+ * @param topic Den aktuella topicen.
+ * @param type Typ av innehåll ("text" eller "file").
+ * @param isLocal Alltid true för lokalt skickade meddelanden.
+ */
+ public NtfyMessageDto(String message, String topic, String type, boolean isLocal) {
+ // Fix: Vi använder 0L för att explicit kasta 0 som en long
+ this(null, 0L, "message", topic, type, message, isLocal);
+ }
+
+ // Kort konstruktor för att bara skicka en meddelandetext (kanske inte används men fixas för konsekvens)
+ public NtfyMessageDto(String message) {
+ this(null, 0L, "message", null, "text", message, true);
+ }
+
+ @Override
+ public String toString(){
+ return message;
+ }
+
+}
+
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 71574a27..d9d383d6 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -2,6 +2,10 @@
requires javafx.controls;
requires javafx.fxml;
+ requires java.net.http;
+ requires com.fasterxml.jackson.databind;
+ requires io.github.cdimascio.dotenv.java;
+
opens com.example to javafx.fxml;
exports com.example;
}
\ No newline at end of file
diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml
index 20a7dc82..c392392e 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -1,9 +1,67 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..04dadbe0
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,86 @@
+package com.example;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import javafx.beans.property.StringProperty;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+@WireMockTest
+class HelloModelTest {
+
+ private static final String DEFAULT_TOPIC = "mytopic";
+
+ // I HelloModelTest.java
+ @Test
+ @DisplayName("Given a model with messageToSend when calling sendMessage then send method on connection is called")
+ void sendMessageCallsConnectionWithMsgToSend() {
+ // Arrange - Given
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+ String expectedMessage = "Hello World";
+ model.setMessageToSend(expectedMessage);
+
+ // Act - When
+ model.sendMessage();
+
+ // Assert - Then
+ // 1. Verifiera att anslutningen anropades med rätt meddelande:
+ assertThat(spy.getLastSentMessage()).isEqualTo(expectedMessage);
+
+ // 2. Verifiera att det skickade meddelandet lades till i modellens lista (lokal uppdatering):
+ assertThat(model.getMessages()).hasSize(1);
+ assertThat(model.getMessages().get(0).message()).isEqualTo(expectedMessage);
+ // Verifiera även att det markerades som lokalt skickat
+ assertThat(model.getMessages().get(0).isLocal()).isTrue();
+ }
+
+ @Test
+ void sendMessageToFakeServer(WireMockRuntimeInfo wmRuntimeInfo) {
+ var con = new NtfyConnectionImpl("http://localhost:" + wmRuntimeInfo.getHttpPort());
+ stubFor(post("/" + DEFAULT_TOPIC).willReturn(ok()));
+ var model = new HelloModel(con);
+ model.setMessageToSend("Hello World");
+
+
+ model.sendMessage();
+
+ //Verify call made to server
+ verify(postRequestedFor(urlEqualTo("/" + DEFAULT_TOPIC))
+ .withRequestBody(matching("Hello World")));
+ }
+
+
+
+
+ @Test
+ void receiveMessageIsAddedToObservableList() {
+ //Arrange Given
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+ var testMessage = "This is a test message";
+
+ var testDto=new NtfyMessageDto(
+ "id-123",
+ System.currentTimeMillis(),
+ "message",
+ DEFAULT_TOPIC,
+ testMessage);
+
+ assertThat(model.getMessages()).isEmpty();
+
+ //Act When
+ spy.messageHandler.accept(testDto);
+ //Assert Then
+ assertThat(model.getMessages()).hasSize(1);
+ // KORRIGERING: Använder NtfyMessageDto.message()
+ assertThat(model.getMessages().get(0).message()).isEqualTo(testMessage);
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/NtfyConnectionSpy.java b/src/test/java/com/example/NtfyConnectionSpy.java
new file mode 100644
index 00000000..705c3f5f
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,104 @@
+package com.example;
+
+import java.io.File;
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection {
+ private String lastSentMessage = null;
+ private String lastSentTopic = null;
+ private File lastSentFile = null;
+ private boolean connectCalled = false;
+ Consumer messageHandler = null;
+ private String lastConnectTopic = null;
+
+ /**
+ * Återställer spionens tillstånd.
+ */
+ public void reset() {
+ lastSentMessage = null;
+ lastSentTopic = null;
+ lastSentFile = null;
+ connectCalled = false;
+ messageHandler = null;
+ lastConnectTopic = null;
+ }
+
+ // --- Implementering av NtfyConnection Interface ---
+
+ @Override
+ public String getTopic() {
+ // En spion kan returnera den senaste anslutna topicen, eller en tom sträng om inte relevant
+ return lastConnectTopic != null ? lastConnectTopic : "";
+ }
+
+ @Override
+ public void connect(String topic, Consumer messageHandler) {
+ this.connectCalled = true;
+ this.lastConnectTopic = topic;
+ this.messageHandler = messageHandler;
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+
+ }
+
+
+ @Override
+ public boolean send(String message, String topic) {
+ // SPIONLOGIK: Spara det skickade meddelandet och ämnet
+ this.lastSentMessage = message;
+ this.lastSentTopic = topic;
+ return true; // Simulerar framgångsrikt skickande
+ }
+
+ @Override
+ public boolean sendFile(File file, String topic) { // RETURTYPEN ÄNDRAD TILL BOOLEAN
+ // SPIONLOGIK: Spara den skickade filen och ämnet
+ this.lastSentFile = file;
+ this.lastSentTopic = topic;
+ return true; // Simulerar framgångsrikt skickande
+ }
+
+ // --- Metoder för att simulera mottagande av meddelanden (för testning) ---
+
+ /**
+ * Simulerar att ett meddelande tas emot från servern och anropar
+ * den ursprungliga messageHandler.
+ * @param messageDto Meddelandet som ska simuleras att tas emot.
+ */
+ public void simulateMessageReceived(NtfyMessageDto messageDto) {
+ if (messageHandler != null) {
+ messageHandler.accept(messageDto);
+ }
+ }
+
+ // --- Getters för testverifieringar Topic behålls för flexibilitet att skapa fler chattrum---
+
+ public String getLastSentMessage() {
+ return lastSentMessage;
+ }
+
+ public String getLastSentTopic() {
+ return lastSentTopic;
+ }
+
+ public File getLastSentFile() {
+ return lastSentFile;
+ }
+
+ public boolean isConnectCalled() {
+ return connectCalled;
+ }
+
+ public String getLastConnectTopic() {
+ return lastConnectTopic;
+ }
+
+ /**
+ * Används för att kontrollera att en messageHandler sattes.
+ */
+ public boolean hasMessageHandler() {
+ return messageHandler != null;
+ }
+}
\ No newline at end of file