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..203cdfa6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,12 +17,29 @@
25
+
+
+ tools.jackson.core
+ jackson-databind
+ 3.0.1
+
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
org.junit.jupiter
junit-jupiter
${junit.jupiter.version}
test
+
+ org.wiremock
+ wiremock
+ 4.0.0-beta.15
+ test
+
org.assertj
assertj-core
@@ -45,6 +62,12 @@
javafx-fxml
${javafx.version}
+
+ org.awaitility
+ awaitility
+ 4.3.0
+ test
+
@@ -63,6 +86,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..f630db9c 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,148 @@
package com.example;
+import javafx.application.Platform;
+import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
-import javafx.scene.control.Label;
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
/**
* Controller layer: mediates between the view (FXML) and the model.
+ * Handles updates of the chat-window.
*/
public class HelloController {
- private final HelloModel model = new HelloModel();
+ //En model skapas som i bakgrunden är en lista och håller koll på meddelanden
+ private final HelloModel model = new HelloModel(new NtfyConnectionImpl());
+ //@FXML kopplingar
@FXML
- private Label messageLabel;
+ private Button connectToServer;
+ @FXML
+ private Button disconnectFromServer;
+
+ //Kopplar ett textfält från FXML där användaren skriver ett meddelande
+ @FXML
+ private TextField messageInput;
+
+ //Kopplar en knapp från FXML som klickas på för att skicka meddelandet
+ @FXML
+ private Button sendButton;
+
+
+ //Ytan för alla meddelanden som visas
+ @FXML
+ private ListView chatBox;
+
+ //Metoden körs automatiskt när appen startar
@FXML
private void initialize() {
- if (messageLabel != null) {
- messageLabel.setText(model.getGreeting());
+ //todo: Initialisera uppkopplingsknapparna för server-anslutning
+ //todo: metod för nätverksuppkoppling?
+
+ //Sätter ursprungstillståndet (default) för skicka-knappen
+
+ updateSendButtonState();
+
+ //Lägger till en lyssnare för att uppdatera knappen vid inmatning av text
+ messageInput.textProperty().addListener((observable, oldValue, newValue) -> updateSendButtonState());
+
+ //Om användaren trycker på Enter eller klickar med musen -> skicka meddelandet
+ messageInput.setOnAction((event) -> sendMessageToModel());
+ sendButton.setOnAction(event -> sendMessageToModel());
+
+ disconnectFromServer.setOnAction(event -> setDisconnectFromServer());
+ connectToServer.setOnAction(event -> setConnectToServer());
+
+ disconnectFromServer.setDisable(true);
+
+ //model.receiveMessage();
+ //Styr hur varje meddelande ska visas i chatboxen
+ chatBox.setCellFactory(listView -> new ListCell<>() {
+ @Override
+ protected void updateItem(NtfyMessageDto item, boolean empty) {
+ super.updateItem(item, empty);
+ //Kräver en null check då JavaFX återanvänder cellerna
+ if (item == null || empty) {
+ setText(null);
+ setGraphic(null);
+ }
+ else{
+ //Skapar en label med meddelande-texten och sätter en stil från css
+ Label label = new Label(item.message());
+ label.getStyleClass().add("message-bubble");
+
+ String time = item.formattedTime();
+ Label labelTime = new Label(time);
+ labelTime.getStyleClass().add("time-stamp");
+
+ //Layout
+ VBox messageBox = new VBox(label, labelTime);
+ messageBox.setSpacing(2);
+
+ //Vänster eller höger i ListView
+ HBox hbox = new HBox(messageBox);
+ hbox.setMaxWidth(chatBox.getWidth()-20);
+
+ String messagePosition = item.message();
+ if (messagePosition != null && messagePosition.startsWith("User:")) {
+ hbox.setAlignment(javafx.geometry.Pos.CENTER_RIGHT);
+ label.getStyleClass().add("outgoing-message");
+ } else {
+ hbox.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
+ label.getStyleClass().add("incoming-message");
+ }
+ setGraphic(hbox);
+ }
+ }
+ });
+ //Kopplar Listan i view med ObservableList i HelloModel
+ chatBox.setItems(model.getMessages());
+ //Flyttar Platform.runlater till controller på grund av runtimeexeption när tester körs, även för en mer solid MVC
+ model.getMessages().addListener((ListChangeListener) changes -> {
+ chatBox.refresh();
+ });
+ }
+
+ private void sendMessageToModel() {
+ String outgoingMessage = messageInput.getText().trim();
+ //Kontrollerar om text-fältet är tomt
+ if (!outgoingMessage.isEmpty()) {
+ model.sendMessage("User: " + outgoingMessage);
+ //tömmer sedan fältet där text matas in(prompt-meddelande visas igen)
+ messageInput.clear();
}
}
-}
+
+ private void updateSendButtonState() {
+ // Kollar om texten, efter att ha tagit bort ledande/efterföljande mellanslag, är tom.
+ boolean isTextPresent = !messageInput.getText().trim().isEmpty();
+
+ //Nytt villkor för button för att ej kunna skicka meddelanden till servern om ej connectad
+ boolean isConnected = !disconnectFromServer.isDisabled();
+
+ // Sätt disable till TRUE om det INTE finns text.
+ sendButton.setDisable(!isTextPresent || !isConnected);
+ }
+
+ //Starta prenumeration via model och uppdaterar button
+ public void setConnectToServer() {
+ if(disconnectFromServer.isDisable()) {
+ model.receiveMessage();
+ connectToServer.setDisable(true);
+ disconnectFromServer.setDisable(false);
+ updateSendButtonState();
+ }
+ }
+
+ //Stoppar prenumerationen och uppdaterar button
+ public void setDisconnectFromServer() {
+ model.stopSubscription();
+ connectToServer.setDisable(false);
+ disconnectFromServer.setDisable(true);
+ updateSendButtonState();
+ }
+}
\ 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..3578b648 100644
--- a/src/main/java/com/example/HelloFX.java
+++ b/src/main/java/com/example/HelloFX.java
@@ -1,19 +1,24 @@
package com.example;
+import io.github.cdimascio.dotenv.Dotenv;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
+import javafx.scene.image.Image;
import javafx.stage.Stage;
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);
- stage.setTitle("Hello MVC");
+ Scene scene = new Scene(root, 540, 650);
+ stage.setTitle("CatCode Messenger");
+ stage.getIcons().add(new Image(getClass().getResourceAsStream("/coolCat.png")));
+
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..67bbddf2 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,86 @@
package com.example;
+import io.github.cdimascio.dotenv.Dotenv;
+import javafx.application.Platform;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+import java.io.IOException;
+
+
/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {
/**
- * Returns a greeting based on the current Java and JavaFX versions.
+ * Handles and returns a list of messages observed by JavaFX
+ * Stores, changes and returns data.
*/
- public String getGreeting() {
- String javaVersion = System.getProperty("java.version");
- String javafxVersion = System.getProperty("javafx.version");
- return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
+
+ //Lista som håller alla meddelanden
+ //FXCollections.observableArrayList() = Nyckel som gör listan ändrings-bar och uppdaterar GUIt
+ private final ObservableList messages = FXCollections.observableArrayList();
+
+//Kopplar upp till nätverket , används för att skicka och ta emot meddelanden
+ private final NtfyConnection connection;
+ //Innehåller meddelandet som ska skickas, kopplat till GUI via SimpleStringProperty
+ private final StringProperty messageToSend = new SimpleStringProperty();
+ //Fält för att kunna styra anslutningen
+ private Subscription subscription = null;
+
+ //Konstruktorn tar emot nätverkskoppling, antingen ett test via spy eller en riktig via impl
+ public HelloModel(NtfyConnection connection) {
+
+ this.connection = connection;
+ //subscription = receiveMessage(); //subscription startar automatiskt när modellen skapas
+ }
+
+ //getter från private, används av controller för att koppla til ListView
+ public ObservableList getMessages() {
+ return messages;
+ }
+ //test
+ public String getMessageToSend() {
+ return messageToSend.get();
+ }
+//Getter från private för meddelandet som ska skickas
+ public StringProperty messageToSendProperty() {
+ return messageToSend;
+ }
+//Sätter meddelande för tester
+ public void setMessageToSend(String message) {
+ messageToSend.set(message);
+ }
+
+ //Sätter meddelandet till inkommande parameter från test, eller controller (connection skickar till nätverket)
+ public void sendMessage(String message) {
+
+ messageToSend.set(message);
+ connection.send(messageToSend.get());
+
+ }
+
+ //Startar en prenumeration på inkommande meddelnaden,
+ //Returnerar ett Subscription-objekt så den kan stoppas
+ public Subscription receiveMessage() {
+if(subscription != null && subscription.isOpen()) {
+ return this.subscription;
+}
+ return subscription = connection.receive(messages::add);
+
+
+ }
+
+ public void stopSubscription() {
+ if (subscription != null && subscription.isOpen())
+ try{
+ subscription.close();
+ } catch(IOException e) {
+ System.out.println("Error closing subscription" + e.getMessage());
+ }
}
}
+
+
diff --git a/src/main/java/com/example/NtfyConnection.java b/src/main/java/com/example/NtfyConnection.java
new file mode 100644
index 00000000..39fe8105
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,17 @@
+package com.example;
+
+
+import java.nio.file.Path;
+import java.util.function.Consumer;
+
+public interface NtfyConnection {
+
+ //Skicka ett meddelande till servern
+ boolean send(String message);
+
+ boolean sendFile(Path file, String messageWithFile);
+
+ //Startar en prenumeration och tar emot en consumer som ska köras varje gång ett meddelande kommer
+ Subscription receive(Consumer consumer);
+
+}
diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java
new file mode 100644
index 00000000..4765ff36
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,88 @@
+package com.example;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import javafx.application.Platform;
+import tools.jackson.databind.ObjectMapper;
+
+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.Path;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+public class NtfyConnectionImpl implements NtfyConnection {
+ //Adressen till servern
+ private final String hostName;
+ //För att skicka och ta emot HTTP-meddelanden
+ private final HttpClient http = HttpClient.newHttpClient();
+ //För att konvertera JSON till java objekt
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public NtfyConnectionImpl() {
+ Dotenv dotenv = Dotenv.load();
+ hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
+ }
+
+ public NtfyConnectionImpl(String hostName) {
+ this.hostName = hostName;
+ }
+
+ //Skickar ett meddelande till servern via POST
+ @Override
+ public boolean send(String message) {
+ //Send message to client - HTTP meddelande
+ String inputMessage = Objects.requireNonNull(message);
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(inputMessage))
+ .header("Cache", "no")
+ .uri(URI.create(hostName + "/catChat"))
+ .build();
+ try {
+ var response = http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
+ return true;
+ } catch (IOException e) {
+ System.out.println("Error sending message");
+ } catch (InterruptedException e) {
+ System.out.println("Sending message interrupted");
+ }
+ return false;
+ }
+
+ @Override
+ public boolean sendFile(Path file, String messageWithFile) {
+ return false;
+ }
+
+
+ //Skapar en asynkron (flera trådar) GET-stream
+ //VArje rad ändras till ett NtfyMessageDTO och skickas till messageHandler, hopp in i model
+ //Returnerar ett objekt av Subscription som kan stoppa streamen(connected.cancel(true))
+ @Override
+ public Subscription receive(Consumer messageHandler) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/catChat/json"))
+ .build();
+
+ var connected = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+ .thenAccept(response -> response.body()
+ .map(s -> mapper.readValue(s, NtfyMessageDto.class))
+ .filter(message -> message.event().equals("message"))
+ .peek(System.out::println)
+ .forEach(message -> Platform.runLater(()-> messageHandler.accept(message))));
+ return new Subscription() {
+ @Override
+ public void close() {
+ connected.cancel(true);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return !connected.isDone();
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java
new file mode 100644
index 00000000..c72e02e8
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,25 @@
+package com.example;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record NtfyMessageDto(String id, Long time, String event, String topic, String message) {
+ //Convenience Konstruktor - som en genväg till record fält
+// public NtfyMessageDto(String message) {
+// this("thisID", System.currentTimeMillis()/1000, "message", "catChat", message);
+// }
+
+
+ public String formattedTime() {
+ if (time == null) return "";
+ return DateTimeFormatter.ofPattern("HH:mm")
+ .format(Instant.ofEpochSecond(time)
+ .atZone(ZoneId.of("Europe/Stockholm")));
+
+ }
+
+}
diff --git a/src/main/java/com/example/Subscription.java b/src/main/java/com/example/Subscription.java
new file mode 100644
index 00000000..7fb9602b
--- /dev/null
+++ b/src/main/java/com/example/Subscription.java
@@ -0,0 +1,13 @@
+package com.example;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public interface Subscription extends Closeable {
+ //Stoppar en subscription
+ @Override
+ void close() throws IOException;
+
+ //Meddelar om Subscription är öppen
+ boolean isOpen();
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 71574a27..89e041cb 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,6 +1,10 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
+ requires io.github.cdimascio.dotenv.java;
+ requires java.net.http;
+ requires tools.jackson.databind;
+ requires javafx.graphics;
opens com.example to javafx.fxml;
exports com.example;
diff --git a/src/main/resources/cat_pattern.png b/src/main/resources/cat_pattern.png
new file mode 100644
index 00000000..e283fe62
Binary files /dev/null and b/src/main/resources/cat_pattern.png differ
diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml
index 20a7dc82..596bcc8b 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -1,9 +1,31 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/coolCat.png b/src/main/resources/coolCat.png
new file mode 100644
index 00000000..1dbecae2
Binary files /dev/null and b/src/main/resources/coolCat.png differ
diff --git a/src/main/resources/css/style.css b/src/main/resources/css/style.css
new file mode 100644
index 00000000..4fcf9482
--- /dev/null
+++ b/src/main/resources/css/style.css
@@ -0,0 +1,108 @@
+/* --- ROOT/GLOBAL STYLING --- */
+.root {
+ -fx-background-image: url("../cat_pattern.png");
+ -fx-background-size: cover;
+ -fx-padding: 20;
+}
+
+/* --- TOP --- */
+.top-bar {
+ -fx-pref-height: 50px;
+ -fx-background-color: transparent;
+}
+
+.title-bar {
+ -fx-text-fill: #d91d5f;
+ -fx-font-family: "Britannic Bold";
+ -fx-font-size: 36;
+}
+
+/* --- CENTER (Chatt Bubblor & Container) --- */
+
+/* Containern som håller alla meddelanden (VBox inuti ScrollPane) */
+.chat-box-container {
+ -fx-spacing: 10px;
+ -fx-padding: 10px;
+ -fx-background: transparent;
+ -fx-background-color: rgba(255, 255, 255, 0.6);
+ -fx-background-radius: 5;
+}
+
+.list-cell {
+ -fx-background-color: transparent;
+}
+
+.message-bubble {
+ -fx-background-color: rgb(217, 29, 95);
+ -fx-text-fill: white;
+ -fx-padding: 8px 12px;
+ -fx-background-radius: 10;
+ -fx-wrap-text: true;
+ -fx-max-width: 200px;
+ -fx-font-family: "Century Gothic";
+}
+
+.incoming-message {
+ -fx-background-color: black;
+ -fx-text-fill: white;
+
+}
+
+.outgoing-message {
+ -fx-background-color: #ffffff;
+ -fx-text-fill: #0e0a0a;
+}
+
+.time-stamp {
+ -fx-font-size: 10px;
+ -fx-text-fill: #d91d5f;
+ -fx-padding: 8px 12px;
+}
+
+
+/* --- BOTTOM (Inputfält & Knapp) --- */
+
+/* Containern för inputfältet och Skicka-knappen */
+.input-bar {
+ -fx-spacing: 10px;
+ -fx-padding: 10px;
+ -fx-alignment: CENTER_LEFT;
+}
+
+.text-field {
+ -fx-pref-height: 50px;
+ -fx-background-color: black;
+ -fx-text-fill: #f1eeee;
+ -fx-prompt-text-fill: #efe9e9;
+ -fx-border-color: #444;
+ -fx-border-radius: 5;
+ -fx-padding: 20;
+ -fx-inner-shadow: inset 0 0 5px rgba(255, 255, 255, 0.1);
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.4), 5, 0, 0, 0);
+}
+
+.text-field:focused {
+ -fx-border-color: #d91d5f;
+ /* Lägg till en ljusare inre skugga för en "glöd"-effekt */
+ -fx-inner-shadow: inset 0 0 8px rgba(217, 29, 95, 0.8);
+}
+
+.text-field:hover {
+ -fx-border-color: #d91d5f;
+ -fx-inner-shadow: inset 0 0 8px rgba(217, 29, 95, 0.8);
+}
+
+.button {
+ -fx-background-color: #0e0a0a;
+ -fx-text-fill: #d8cbcb;
+ -fx-border-radius: 5;
+ -fx-padding: 6px 12px;
+}
+.button:hover {
+ -fx-background-color: #3A3A3A;
+ -fx-text-fill: #d91d5f;
+ -fx-border-radius: 5;
+ -fx-padding: 6px 12px;
+}
+
+
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..c6131b3a
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,104 @@
+package com.example;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+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 {
+
+ @Test
+ void sendMessageCallsConnectionWithMessageToSend() {
+ //Arrange
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+ //Act
+ model.setMessageToSend("");
+
+ model.sendMessage("Hello World");
+//Assert
+ assertThat(spy.message).isEqualTo("Hello World");
+ }
+
+ @Test
+ void receiveMessageFromFakeServer(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException, InterruptedException {
+ //Arrange
+ var host = "http://localhost:" + wireMockRuntimeInfo.getHttpPort();
+ var con = new NtfyConnectionImpl(host);
+
+ String fakeMessage = """
+ {"id":"testID","time":1762935416, "event":"keepalive","topic":"catChat", "message":"Filtreras bort"}
+ {"id":"testID","time":1762935416, "event":"message","topic":"catChat", "message":"User: Hej"}
+ """;
+
+
+ //Simulerar en server
+ stubFor(get(urlEqualTo("/catChat/json")).willReturn(aResponse()
+ .withStatus(200)
+ .withBody(fakeMessage)));
+
+// Consumer fakeReceiver = Mockito.mock(Consumer.class);
+// //con.receive(fakeReceiver);
+//
+// ArgumentCaptor captor = ArgumentCaptor.forClass(NtfyMessageDto.class);
+// Subscription subscription = con.receive(fakeReceiver);
+
+// //Act
+
+ var model =new HelloModel(con);
+ model.receiveMessage();
+
+ Thread.sleep(50);
+
+ Awaitility.await()
+ .atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(model.getMessages()).hasSize(1);
+ });
+
+ //Assert
+ assertThat(model.getMessages().getFirst().message()).isEqualTo("User: Hej");
+
+ model.stopSubscription(); //Stänger anslutningen
+ }
+
+ @Test
+ void sendMessageToFakeServer(WireMockRuntimeInfo wmRuntimeInfo) {
+ var con = new NtfyConnectionImpl("http://localhost:" + wmRuntimeInfo.getHttpPort());
+ var model = new HelloModel(con);
+ model.setMessageToSend("");
+ stubFor(post("/catChat").willReturn(ok()));
+
+ model.sendMessage("Hello World");
+
+ WireMock.verify(postRequestedFor(urlEqualTo("/catChat"))
+ .withRequestBody(matching("Hello World")));
+ }
+
+ @Test
+ void messageIsAddedToObservableList() {
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+
+ model.receiveMessage();
+ var testText = new NtfyMessageDto("id1", 15465823L,"Message", "catChat", "Godmorgon");
+ spy.simulateIncomingMessage(testText);
+
+ assertThat(model.getMessages()).extracting(NtfyMessageDto::message).contains("Godmorgon");
+ }
+
+
+}
\ 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..eae41de5
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,53 @@
+package com.example;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection {
+ //Meddelande som skickas
+ String message;
+ //funktionen som ska köras när ett meddelande kommer
+ Consumer consumer;
+
+ //Sparar meddelandet som ska användas i tester för att kontrollera att rätt sak skickas
+ @Override
+ public boolean send(String message) {
+ this.message = message;
+ return false;
+ }
+
+ @Override
+ public boolean sendFile(Path file, String messageWithFile) {
+ return false;
+ }
+
+ //Sparar en consumer och returnerar en falsk Subscription
+ //Sätter consumer till null och returnerar att fake-servern är öppen, detta kan styras via booleanflaggan
+ @Override
+ public Subscription receive(Consumer consumer) {
+ this.consumer = consumer;
+
+ return new Subscription() {
+ private boolean open = true;
+ @Override
+ public void close() {
+ open = false;
+ NtfyConnectionSpy.this.consumer = null;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+ };
+ }
+
+ //Anropar consumer och simulerar att ett meddelande kom in från nätverket
+ public void simulateIncomingMessage(NtfyMessageDto message) {
+ if (consumer != null) {
+ consumer.accept(message);
+ }
+ }
+
+ }