diff --git a/.gitignore b/.gitignore
index 6ac465db..5a54815d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
target/
/.idea/
+.env
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c40f667e..bc863a7a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,28 @@
javafx-fxml
${javafx.version}
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
+
+ tools.jackson.core
+ jackson-databind
+ 3.0.1
+
+
+ org.wiremock
+ wiremock
+ 4.0.0-beta.15
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.12
+ test
+
@@ -55,7 +77,7 @@
com.example.HelloFX
-
+
javafx
true
@@ -65,4 +87,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java
index fdd160a0..1ce67548 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,71 @@
package com.example;
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
import javafx.fxml.FXML;
-import javafx.scene.control.Label;
+import javafx.scene.control.*;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
-/**
- * Controller layer: mediates between the view (FXML) and the model.
- */
public class HelloController {
- private final HelloModel model = new HelloModel();
+ private final HelloModel model = new HelloModel(new NtfyConnectionImpl());
- @FXML
- private Label messageLabel;
+ @FXML private Label messageLabel;
+ @FXML private ListView messageView;
+ @FXML private TextArea messageInput;
+
+ private final DateTimeFormatter timeFormatter =
+ DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
@FXML
private void initialize() {
- if (messageLabel != null) {
- messageLabel.setText(model.getGreeting());
- }
+ //visa välkomstmeddelande
+ messageLabel.setText(model.getGreeting());
+
+ messageView.setItems(model.getMessages());
+
+ messageInput.textProperty().bindBidirectional(model.messageToSendProperty());
+
+ //formatera meddelanden i ListView
+ messageView.setCellFactory(lv -> new ListCell<>() {
+ @Override
+ protected void updateItem(NtfyMessageDto msg, boolean empty) {
+ super.updateItem(msg, empty);
+ if (empty || msg == null) {
+ setText(null);
+ } else {
+ setText("[" + timeFormatter.format(Instant.ofEpochMilli(msg.time())) + "] " + msg.message());
+ }
+ }
+ });
+
+ // Scrolla automatiskt till senaste meddelandet
+ model.getMessages().addListener((javafx.collections.ListChangeListener) change -> {
+ while (change.next()) {
+ if (change.wasAdded()) {
+ Platform.runLater(() -> {
+ int size = messageView.getItems().size();
+ if (size > 0) {
+ messageView.scrollTo(size - 1);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ @FXML
+ private void sendMessage(ActionEvent event) {
+ //skicka asynkront – HelloModel hanterar rensning och callback
+ model.sendMessageAsync(success -> {
+ if (!success) {
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.ERROR, "Kunde inte skicka meddelandet.");
+ alert.show();
+ });
+ }
+ });
}
-}
+}
\ 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..5aaa17af 100644
--- a/src/main/java/com/example/HelloFX.java
+++ b/src/main/java/com/example/HelloFX.java
@@ -12,7 +12,7 @@ public class HelloFX extends Application {
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);
+ Scene scene = new Scene(root, 1280, 480);
stage.setTitle("Hello MVC");
stage.setScene(scene);
stage.show();
diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java
index 385cfd10..76bb9c78 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,74 @@
package com.example;
-/**
- * Model layer: encapsulates application data and business logic.
- */
+import javafx.application.Platform;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import java.util.function.Consumer;
+
public class HelloModel {
- /**
- * Returns a greeting based on the current Java and JavaFX versions.
- */
- public String getGreeting() {
- String javaVersion = System.getProperty("java.version");
- String javafxVersion = System.getProperty("javafx.version");
- return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
- }
-}
+
+ private final NtfyConnection connection;
+ private final ObservableList messages = FXCollections.observableArrayList();
+ private final StringProperty messageToSend = new SimpleStringProperty("");
+
+ public HelloModel(NtfyConnection connection) {
+ this.connection = connection;
+ startReceiving();
+ }
+
+ private void startReceiving() {
+ connection.receive(incoming -> {
+ if (incoming == null || incoming.message() == null || incoming.message().isBlank()) {
+ return;
+ }
+ runOnFx(() -> messages.add(incoming));
+ });
+ }
+
+ public void sendMessageAsync(Consumer callback) {
+ String msg = messageToSend.get();
+ if (msg == null || msg.isBlank()) {
+ callback.accept(false);
+ return;
+ }
+
+ try {
+ connection.send(msg, success -> {
+ if (success) {
+ runOnFx(() -> {
+ if (msg.equals(messageToSend.get())) {
+ messageToSend.set("");
+ }
+ });
+ callback.accept(true);
+ } else {
+ callback.accept(false);
+ }
+ });
+ } catch (Exception e) {
+ // FÅNGA ALLA EXCEPTIONS HÄR!
+ System.err.println("Exception during send: " + e.getMessage());
+ callback.accept(false);
+ }
+ }
+
+ private static void runOnFx(Runnable task) {
+ try {
+ if (Platform.isFxApplicationThread()) {
+ task.run();
+ } else {
+ Platform.runLater(task);
+ }
+ } catch (Exception e) {
+ task.run(); // fallback i tester
+ }
+ }
+
+ public ObservableList getMessages() { return messages; }
+ public String getMessageToSend() { return messageToSend.get(); }
+ public StringProperty messageToSendProperty() { return messageToSend; }
+ public void setMessageToSend(String v) { messageToSend.set(v); }
+ public String getGreeting() { return "Welcome to ChatApp"; }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/ManyParameters.java b/src/main/java/com/example/ManyParameters.java
new file mode 100644
index 00000000..9efe0e71
--- /dev/null
+++ b/src/main/java/com/example/ManyParameters.java
@@ -0,0 +1,19 @@
+package com.example;
+
+public class ManyParameters {
+
+ public ManyParameters(String computerName, int timeout,
+ String method, int size, byte[] data) {
+
+ }
+
+
+ static void main() {
+ ManyParametersBuilder builder = new ManyParametersBuilder();
+ builder
+ .setComputerName("localhost") //Fluent API
+ .setTimeout(10)
+ .setSize(0)
+ .createManyParameters();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/ManyParametersBuilder.java b/src/main/java/com/example/ManyParametersBuilder.java
new file mode 100644
index 00000000..fd49920d
--- /dev/null
+++ b/src/main/java/com/example/ManyParametersBuilder.java
@@ -0,0 +1,38 @@
+package com.example;
+
+public class ManyParametersBuilder {
+ private String computerName;
+ private int timeout = 0;
+ private String method;
+ private int size = 0;
+ private byte[] data = null;
+
+ public ManyParametersBuilder setComputerName(String computerName) {
+ this.computerName = computerName;
+ return this;
+ }
+
+ public ManyParametersBuilder setTimeout(int timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ public ManyParametersBuilder setMethod(String method) {
+ this.method = method;
+ return this;
+ }
+
+ public ManyParametersBuilder setSize(int size) {
+ this.size = size;
+ return this;
+ }
+
+ public ManyParametersBuilder setData(byte[] data) {
+ this.data = data;
+ return this;
+ }
+
+ public ManyParameters createManyParameters() {
+ return new ManyParameters(computerName, timeout, method, size, data);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/NtfyConnection.java b/src/main/java/com/example/NtfyConnection.java
new file mode 100644
index 00000000..d956cc24
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,12 @@
+package com.example;
+
+import java.util.function.Consumer;
+
+interface NtfyConnection {
+ void send(String message, Consumer callback);
+ void receive(Consumer handler);
+ }
+
+ //public boolean send(String message);
+
+ //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..cb50582e
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,87 @@
+package com.example;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import tools.jackson.databind.ObjectMapper;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+public class NtfyConnectionImpl implements NtfyConnection {
+
+ private final HttpClient http;
+ private final String hostName;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public NtfyConnectionImpl() {
+ Dotenv dotenv = Dotenv.load();
+ hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
+ this.http = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ }
+
+ public NtfyConnectionImpl(String hostName) {
+ this.hostName = hostName;
+ this.http = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(5))
+ .build();
+ }
+
+ @Override
+ public void send(String message, Consumer callback) {
+ HttpRequest request = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(message))
+ .uri(URI.create(hostName + "/mytopic"))
+ .header("Cache", "no")
+ .timeout(Duration.ofSeconds(10)) //request timeout
+ .build();
+
+ http.sendAsync(request, HttpResponse.BodyHandlers.discarding())
+ .thenAccept(response -> {
+ boolean success = response.statusCode() >= 200 && response.statusCode() < 300;
+ callback.accept(success);
+ })
+ .exceptionally(throwable -> {
+ System.err.println("Error sending message: " + throwable.getMessage());
+ callback.accept(false);
+ return null;
+ });
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ HttpRequest request = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/mytopic/json"))
+ .timeout(Duration.ofSeconds(30)) //timeout för receive
+ .build();
+
+ http.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
+ .thenAccept(response -> {
+ try {
+ response.body()
+ .map(line -> {
+ try {
+ return mapper.readValue(line, NtfyMessageDto.class);
+ } catch (Exception e) {
+ System.err.println("Failed to parse message: " + line);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .forEach(messageHandler);
+ } catch (Exception e) {
+ System.err.println("Stream processing error: " + e.getMessage());
+ }
+ })
+ .exceptionally(ex -> {
+ System.err.println("Error receiving messages: " + ex.getMessage());
+ return null;
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java
new file mode 100644
index 00000000..697cccac
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,7 @@
+package com.example;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record NtfyMessageDto(String id, long time, String event, String topic, String message) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/Singelton.java b/src/main/java/com/example/Singelton.java
new file mode 100644
index 00000000..b3685a01
--- /dev/null
+++ b/src/main/java/com/example/Singelton.java
@@ -0,0 +1,14 @@
+package com.example;
+
+public class Singelton {
+
+ private final static Singelton instance = new Singelton();
+
+ private Singelton(){
+
+ }
+
+ public static Singelton getInstance(){
+ return instance;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 71574a27..2b046ce2 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,6 +1,9 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
+ requires tools.jackson.databind;
+ requires io.github.cdimascio.dotenv.java;
+ requires java.net.http;
opens com.example to javafx.fxml;
exports com.example;
diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml
index 20a7dc82..63851ee9 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -1,9 +1,30 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..999e6f91
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,321 @@
+package com.example;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import javafx.application.Platform;
+import javafx.collections.ListChangeListener;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WireMockTest
+class HelloModelTest {
+
+ @BeforeAll
+ static void setupJavaFX() {
+ try {
+ if (!Platform.isFxApplicationThread()) {
+ Platform.startup(() -> {
+ });
+ }
+ } catch (IllegalStateException | UnsupportedOperationException e) {
+ System.out.println("Headless environment detected – skipping JavaFX startup");
+ }
+ }
+
+ // successful send tests
+ @Test
+ void shouldSendMessageThroughConnection() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+ model.setMessageToSend("Hello World");
+
+ CountDownLatch latch = new CountDownLatch(1);
+ model.sendMessageAsync(success -> latch.countDown());
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for message send").isTrue();
+ assertThat(connectionSpy.message).isEqualTo("Hello World");
+ }
+
+ @Test
+ void shouldHandleMultipleConsecutiveSends() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+
+ CountDownLatch latch = new CountDownLatch(2);
+ boolean[] results = new boolean[2];
+
+ model.setMessageToSend("First");
+ model.sendMessageAsync(success -> {
+ results[0] = success;
+ latch.countDown();
+ });
+
+ model.setMessageToSend("Second");
+ model.sendMessageAsync(success -> {
+ results[1] = success;
+ latch.countDown();
+ });
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for consecutive sends").isTrue();
+ assertThat(results[0]).isTrue();
+ assertThat(results[1]).isTrue();
+ assertThat(connectionSpy.message).isEqualTo("Second");
+ }
+
+ // invalid send tests
+ @Test
+ void shouldRejectBlankMessages() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+
+ String[] invalidInputs = {"", null};
+
+ for (String input : invalidInputs) {
+ model.setMessageToSend(input);
+ CountDownLatch latch = new CountDownLatch(1);
+ boolean[] wasSuccessful = new boolean[1];
+
+ model.sendMessageAsync(success -> {
+ wasSuccessful[0] = success;
+ latch.countDown();
+ });
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for blank message rejection").isTrue();
+ assertThat(wasSuccessful[0]).isFalse();
+ assertThat(connectionSpy.message).isNull();
+ }
+ }
+
+ @Test
+ void shouldFailWhenSendingEmptyText() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+ model.setMessageToSend("");
+
+ CountDownLatch latch = new CountDownLatch(1);
+ boolean[] wasSuccessful = new boolean[1];
+
+ model.sendMessageAsync(success -> {
+ wasSuccessful[0] = success;
+ latch.countDown();
+ });
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for empty text rejection").isTrue();
+ assertThat(wasSuccessful[0]).isFalse();
+ assertThat(connectionSpy.message).isNull();
+ }
+
+ @Test
+ void shouldFailWhenSendingNullMessage() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+ model.setMessageToSend(null);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ boolean[] wasSuccessful = new boolean[1];
+
+ model.sendMessageAsync(success -> {
+ wasSuccessful[0] = success;
+ latch.countDown();
+ });
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for null message rejection").isTrue();
+ assertThat(wasSuccessful[0]).isFalse();
+ assertThat(connectionSpy.message).isNull();
+ }
+
+ // error handling tests
+ @Test
+ void shouldReturnFailureWhenConnectionFails() throws InterruptedException {
+ NtfyConnection failingConn = new NtfyConnection() {
+ @Override
+ public void send(String message, Consumer callback) {
+ callback.accept(false);
+ }
+ @Override
+ public void receive(Consumer messageHandler) { }
+ };
+ HelloModel model = new HelloModel(failingConn);
+ model.setMessageToSend("Fail this message");
+
+ CountDownLatch latch = new CountDownLatch(1);
+ boolean[] wasSuccessful = new boolean[1];
+
+ model.sendMessageAsync(success -> {
+ wasSuccessful[0] = success;
+ latch.countDown();
+ });
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for connection failure").isTrue();
+ assertThat(wasSuccessful[0]).isFalse();
+ assertThat(model.getMessageToSend()).isEqualTo("Fail this message");
+ }
+
+ @Test
+ void shouldHandleExceptionsDuringSend() throws InterruptedException {
+ NtfyConnection throwingConn = new NtfyConnection() {
+ @Override
+ public void send(String message, Consumer callback) {
+ throw new RuntimeException("Simulated crash");
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ }
+ };
+ HelloModel model = new HelloModel(throwingConn);
+ model.setMessageToSend("Crash this");
+
+ CountDownLatch latch = new CountDownLatch(1);
+ boolean[] wasSuccessful = new boolean[1];
+
+ //wrappa i try-catch för att fånga exception från connection
+ try {
+ model.sendMessageAsync(success -> {
+ wasSuccessful[0] = success;
+ latch.countDown();
+ });
+ } catch (Exception e) {
+ wasSuccessful[0] = false;
+ latch.countDown();
+ }
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for exception handling").isTrue();
+ assertThat(wasSuccessful[0]).isFalse();
+ }
+
+ // receiving messages tests
+ @Test
+ void shouldAddIncomingMessageToList() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+ NtfyMessageDto incomingMsg = new NtfyMessageDto("Test", 1, "message", "myroom", "Test");
+
+ AtomicBoolean messageReceived = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ model.getMessages().addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ if (c.wasAdded()) {
+ messageReceived.set(true);
+ latch.countDown();
+ }
+ }
+ });
+
+ //give the listener time to attach
+ Thread.sleep(100);
+
+ connectionSpy.simulateIncoming(incomingMsg);
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for incoming message").isTrue();
+ assertThat(messageReceived.get()).isTrue();
+ assertThat(model.getMessages()).contains(incomingMsg);
+ }
+
+ @Test
+ void shouldDiscardNullIncomingMessage() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ model.getMessages().addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ if (c.wasAdded()) latch.countDown();
+ }
+ });
+
+ Thread.sleep(100);
+ connectionSpy.simulateIncoming(null);
+
+ boolean messageAdded = latch.await(2, TimeUnit.SECONDS);
+ assertThat(messageAdded).isFalse();
+ assertThat(model.getMessages()).isEmpty();
+ }
+
+ @Test
+ void shouldIgnoreMessagesWithBlankContent() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ model.getMessages().addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ if (c.wasAdded()) latch.countDown();
+ }
+ });
+
+ Thread.sleep(100);
+
+ NtfyMessageDto whitespaceMsg = new NtfyMessageDto("id1", 1, "message", "room", " ");
+ NtfyMessageDto emptyMsg = new NtfyMessageDto("id2", 2, "message", "room", "");
+
+ connectionSpy.simulateIncoming(whitespaceMsg);
+ connectionSpy.simulateIncoming(emptyMsg);
+
+ boolean messageAdded = latch.await(2, TimeUnit.SECONDS);
+ assertThat(messageAdded).isFalse();
+ assertThat(model.getMessages()).isEmpty();
+ }
+
+ @Test
+ void shouldRejectAllInvalidIncomingMessages() throws InterruptedException {
+ NtfyConnectionSpy connectionSpy = new NtfyConnectionSpy();
+ HelloModel model = new HelloModel(connectionSpy);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ model.getMessages().addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ if (c.wasAdded()) latch.countDown();
+ }
+ });
+
+ Thread.sleep(100);
+
+ connectionSpy.simulateIncoming(new NtfyMessageDto("id1", 1, "message", "room", ""));
+ connectionSpy.simulateIncoming(new NtfyMessageDto("id2", 2, "message", "room", " "));
+ connectionSpy.simulateIncoming(null);
+
+ boolean messageAdded = latch.await(2, TimeUnit.SECONDS);
+ assertThat(messageAdded).isFalse();
+ assertThat(model.getMessages()).isEmpty();
+ }
+
+ //integration test
+ @Test
+ void shouldCommunicateWithMockedServer(WireMockRuntimeInfo wmInfo) throws InterruptedException {
+ stubFor(post("/mytopic").willReturn(ok()));
+ stubFor(get("/mytopic/json").willReturn(ok().withBody("")));
+
+ NtfyConnectionImpl connection = new NtfyConnectionImpl("http://localhost:" + wmInfo.getHttpPort());
+ HelloModel model = new HelloModel(connection);
+
+ Thread.sleep(100);
+
+ model.setMessageToSend("Hello World");
+
+ CountDownLatch latch = new CountDownLatch(1);
+ model.sendMessageAsync(success -> latch.countDown());
+
+ boolean completed = latch.await(5, TimeUnit.SECONDS);
+ assertThat(completed).as("Timed out waiting for server communication").isTrue();
+ verify(postRequestedFor(urlEqualTo("/mytopic"))
+ .withRequestBody(matching("Hello World")));
+ }
+}
\ 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..162bb612
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,23 @@
+package com.example;
+
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection {
+ public String message;
+ private Consumer handler;
+
+ @Override
+ public void send(String message, Consumer callback) {
+ this.message = message;
+ callback.accept(true);
+ }
+
+ @Override
+ public void receive(Consumer h) {
+ this.handler = h;
+ }
+
+ public void simulateIncoming(NtfyMessageDto msg) {
+ if (handler != null) handler.accept(msg);
+ }
+}
\ No newline at end of file