Skip to content

Commit fcd0249

Browse files
Merge pull request #32 from ithsjava25/tests
Tests
2 parents 21c51e8 + 7c423e9 commit fcd0249

File tree

7 files changed

+296
-3
lines changed

7 files changed

+296
-3
lines changed

pom.xml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,54 @@
4545
<artifactId>javafx-fxml</artifactId>
4646
<version>${javafx.version}</version>
4747
</dependency>
48+
49+
4850
</dependencies>
4951
<build>
5052
<plugins>
53+
54+
<plugin>
55+
<groupId>org.apache.maven.plugins</groupId>
56+
<artifactId>maven-compiler-plugin</artifactId>
57+
<version>3.11.0</version>
58+
<configuration>
59+
<source>25</source>
60+
<target>25</target>
61+
<release>25</release>
62+
</configuration>
63+
</plugin>
64+
5165
<plugin>
5266
<groupId>org.openjfx</groupId>
5367
<artifactId>javafx-maven-plugin</artifactId>
5468
<version>0.0.8</version>
5569
<configuration>
5670
<mainClass>com.example.HelloFX</mainClass>
5771
<options>
58-
<option>--enable-native-access=javafx.graphics</option>
72+
<option>--enable-native-access=javafx.graphics</option>
5973
</options>
6074
<launcher>javafx</launcher>
6175
<stripDebug>true</stripDebug>
6276
<noHeaderFiles>true</noHeaderFiles>
6377
<noManPages>true</noManPages>
6478
</configuration>
6579
</plugin>
80+
81+
<plugin>
82+
<groupId>org.apache.maven.plugins</groupId>
83+
<artifactId>maven-surefire-plugin</artifactId>
84+
<version>3.1.2</version>
85+
<configuration>
86+
<useModulePath>false</useModulePath>
87+
<includes>
88+
<include>**/*Test.java</include>
89+
</includes>
90+
</configuration>
91+
</plugin>
92+
6693
</plugins>
94+
95+
96+
6797
</build>
6898
</project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.example;
2+
3+
import javafx.fxml.FXML;
4+
import javafx.scene.control.ListView;
5+
import javafx.scene.control.TextField;
6+
import javafx.application.Platform;
7+
8+
public class ChatController {
9+
10+
@FXML
11+
private ListView<String> messagesList;
12+
13+
@FXML
14+
private TextField inputField;
15+
16+
private final ChatModel model = new ChatModel();
17+
18+
19+
@FXML
20+
private void onSend() {
21+
String message = inputField.getText().trim();
22+
if (!message.isEmpty()) {
23+
messagesList.getItems().add("Me: " + message);
24+
model.sendMessage(message);
25+
inputField.clear();
26+
}
27+
}
28+
29+
30+
@FXML
31+
private void initialize(){
32+
model.subscribe(msg -> {
33+
Platform.runLater(() -> messagesList.getItems().add("Friend: " + msg));
34+
});
35+
}
36+
37+
38+
39+
40+
41+
42+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.example;
2+
3+
import javafx.application.Platform;
4+
5+
import java.io.BufferedReader;
6+
import java.io.InputStreamReader;
7+
import java.net.URI;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
import java.util.Collections;
12+
import java.util.Set;
13+
import java.util.UUID;
14+
import java.util.concurrent.ConcurrentHashMap;
15+
import java.util.function.Consumer;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
18+
19+
public class ChatModel {
20+
21+
private final String sendUrl;
22+
private final String subscribeUrl;
23+
private final String clientId;
24+
private final Set<String> sentMessages = Collections.newSetFromMap(new ConcurrentHashMap<>());
25+
private final HttpClient httpClient;
26+
private final Consumer<Runnable> platformRunner;
27+
28+
public ChatModel() {
29+
this(HttpClient.newHttpClient(), Platform::runLater);
30+
}
31+
32+
public ChatModel(HttpClient httpClient, Consumer<Runnable> platformRunner) {
33+
String topic = System.getenv().getOrDefault("NTFY_TOPIC", "https://ntfy.sh/newchatroom3");
34+
this.sendUrl = topic;
35+
this.subscribeUrl = topic + "/sse";
36+
this.clientId = UUID.randomUUID().toString();
37+
this.httpClient = httpClient;
38+
this.platformRunner = platformRunner;
39+
}
40+
41+
public void sendMessage(String message) {
42+
sentMessages.add(message);
43+
44+
new Thread(() -> {
45+
try {
46+
Thread.sleep(5000);
47+
} catch (InterruptedException ignored) {
48+
}
49+
sentMessages.remove(message);
50+
}, "SentMessageCleaner").start();
51+
52+
HttpRequest request = HttpRequest.newBuilder()
53+
.uri(URI.create(sendUrl))
54+
.header("Content-Type", "application/json")
55+
.header("X-Client-ID", clientId)
56+
.header("Title", "Friend: ")
57+
.POST(HttpRequest.BodyPublishers.ofString(message))
58+
.build();
59+
60+
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
61+
.thenAccept(response -> System.out.println("Sent, status=" + response.statusCode()))
62+
.exceptionally(e -> {
63+
e.printStackTrace();
64+
return null;
65+
});
66+
}
67+
68+
69+
public void subscribe(Consumer<String> onMessageReceived) {
70+
HttpRequest request = HttpRequest.newBuilder()
71+
.uri(URI.create(subscribeUrl))
72+
.header("Accept", "text/event-stream")
73+
.build();
74+
75+
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
76+
.thenAccept(response -> {
77+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body()))) {
78+
String line;
79+
// boolean firstMessageSkipped = false;
80+
while ((line = reader.readLine()) != null) {
81+
if (line.startsWith("data:")) {
82+
// if (!firstMessageSkipped) {
83+
// firstMessageSkipped = true;
84+
// continue;
85+
// }
86+
87+
String raw = line.substring(5).trim();
88+
String msg = parseMessage(raw);
89+
90+
if (msg != null && !sentMessages.contains(msg)) {
91+
platformRunner.accept(() -> onMessageReceived.accept(msg));
92+
}
93+
}
94+
}
95+
} catch (Exception e) {
96+
e.printStackTrace();
97+
}
98+
})
99+
.exceptionally(e -> { e.printStackTrace(); return null; });
100+
}
101+
102+
public String parseMessage(String data) {
103+
try {
104+
Matcher eventMatcher = Pattern.compile("\"event\"\\s*:\\s*\"(.*?)\"").matcher(data);
105+
if (eventMatcher.find()) {
106+
String event = eventMatcher.group(1);
107+
if (!"message".equals(event)) {
108+
return null;
109+
}
110+
}
111+
112+
Matcher msgMatcher = Pattern.compile("\"message\"\\s*:\\s*\"(.*?)\"").matcher(data);
113+
if (msgMatcher.find()) {
114+
return msgMatcher.group(1).replace("\\\"", "\"");
115+
}
116+
} catch (Exception ignored) {}
117+
return null;
118+
}
119+
120+
}

src/main/java/com/example/HelloFX.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ public class HelloFX extends Application {
1010

1111
@Override
1212
public void start(Stage stage) throws Exception {
13-
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
13+
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("chat-view.fxml"));
1414
Parent root = fxmlLoader.load();
1515
Scene scene = new Scene(root, 640, 480);
16-
stage.setTitle("Hello MVC");
16+
stage.setTitle("Chat App");
1717
stage.setScene(scene);
1818
stage.show();
1919
}

src/main/java/module-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module hellofx {
22
requires javafx.controls;
33
requires javafx.fxml;
4+
requires java.net.http;
5+
46

57
opens com.example to javafx.fxml;
68
exports com.example;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<?import javafx.scene.layout.BorderPane?>
4+
<?import javafx.scene.control.ListView?>
5+
<?import javafx.scene.control.TextField?>
6+
<?import javafx.scene.control.Button?>
7+
<?import javafx.scene.layout.HBox?>
8+
9+
<BorderPane xmlns="http://javafx.com/javafx"
10+
xmlns:fx="http://javafx.com/fxml"
11+
fx:controller="com.example.ChatController">
12+
13+
<center>
14+
<ListView fx:id="messagesList"/>
15+
</center>
16+
17+
<bottom>
18+
<HBox spacing="8" style="-fx-padding: 10;">
19+
<TextField fx:id="inputField" HBox.hgrow="ALWAYS"/>
20+
<Button text="Send" onAction="#onSend"/>
21+
</HBox>
22+
</bottom>
23+
24+
</BorderPane>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.example;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.net.http.HttpClient;
7+
import java.net.http.HttpRequest;
8+
import java.net.http.HttpResponse;
9+
import java.util.concurrent.CompletableFuture;
10+
import java.util.concurrent.atomic.AtomicReference;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import static org.mockito.ArgumentMatchers.any;
14+
import static org.mockito.Mockito.*;
15+
16+
class ChatModelTest {
17+
18+
private HttpClient mockHttpClient;
19+
private ChatModel chatModel;
20+
21+
@BeforeEach
22+
void setUp() {
23+
mockHttpClient = mock(HttpClient.class);
24+
chatModel = new ChatModel(mockHttpClient, Runnable::run);
25+
}
26+
27+
@Test
28+
void testParseMessageReturnsMessage() {
29+
String data = "{\"event\":\"message\",\"message\":\"Hello World\"}";
30+
String result = chatModel.parseMessage(data);
31+
assertEquals("Hello World", result);
32+
}
33+
34+
@Test
35+
void testParseMessageIgnoresNonMessageEvent() {
36+
String data = "{\"event\":\"update\",\"message\":\"Hello World\"}";
37+
String result = chatModel.parseMessage(data);
38+
assertNull(result);
39+
}
40+
41+
@Test
42+
void testSendMessageCallsHttpClient() throws Exception {
43+
HttpResponse<String> mockResponse = mock(HttpResponse.class);
44+
when(mockResponse.statusCode()).thenReturn(200);
45+
46+
CompletableFuture<HttpResponse<String>> future = CompletableFuture.completedFuture(mockResponse);
47+
when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
48+
.thenReturn(future);
49+
50+
chatModel.sendMessage("Hi there");
51+
52+
Thread.sleep(100);
53+
54+
verify(mockHttpClient, times(1))
55+
.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class));
56+
}
57+
58+
@Test
59+
void testSubscribeCallsConsumer() throws Exception {
60+
String sseData = "data:{\"event\":\"message\",\"message\":\"Hello\"}\n";
61+
HttpResponse<java.io.InputStream> mockResponse = mock(HttpResponse.class);
62+
when(mockResponse.body()).thenReturn(new java.io.ByteArrayInputStream(sseData.getBytes()));
63+
64+
CompletableFuture<HttpResponse<java.io.InputStream>> future = CompletableFuture.completedFuture(mockResponse);
65+
when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
66+
.thenReturn(future);
67+
68+
AtomicReference<String> received = new AtomicReference<>();
69+
chatModel.subscribe(received::set);
70+
71+
Thread.sleep(100);
72+
73+
assertEquals("Hello", received.get());
74+
}
75+
}

0 commit comments

Comments
 (0)