|
| 1 | +package org.tinystruct.system; |
| 2 | + |
| 3 | +import org.junit.jupiter.api.*; |
| 4 | +import org.tinystruct.AbstractApplication; |
| 5 | +import org.tinystruct.ApplicationContext; |
| 6 | +import org.tinystruct.net.URLRequest; |
| 7 | +import org.tinystruct.net.URLResponse; |
| 8 | +import org.tinystruct.net.handlers.HTTPHandler; |
| 9 | +import org.tinystruct.system.annotation.Action; |
| 10 | + |
| 11 | +import java.net.Socket; |
| 12 | +import java.net.URI; |
| 13 | + |
| 14 | +import static org.junit.jupiter.api.Assertions.*; |
| 15 | + |
| 16 | +@TestInstance(TestInstance.Lifecycle.PER_CLASS) |
| 17 | +public class NettyHttpServerHttpModeTest { |
| 18 | + |
| 19 | + private static final int TEST_PORT = 18080; |
| 20 | + private static final String BASE_URL = "http://localhost:" + TEST_PORT; |
| 21 | + private NettyHttpServer httpServer; |
| 22 | + private Thread serverThread; |
| 23 | + private TestWebApp app; |
| 24 | + |
| 25 | + @BeforeAll |
| 26 | + public void setUp() throws Exception { |
| 27 | + // Initialize settings |
| 28 | + Settings settings = new Settings(); |
| 29 | + settings.set("default.base_url", "/?q="); |
| 30 | + settings.set("default.language", "en_US"); |
| 31 | + settings.set("charset", "utf-8"); |
| 32 | + |
| 33 | + // Create and install test app |
| 34 | + this.app = new TestWebApp(); |
| 35 | + ApplicationManager.install(this.app, settings); |
| 36 | + |
| 37 | + // Install required applications |
| 38 | + ApplicationManager.install(new Dispatcher()); |
| 39 | + this.httpServer = new NettyHttpServer(); |
| 40 | + ApplicationManager.install(this.httpServer, settings); |
| 41 | + |
| 42 | + // Start server in a separate thread |
| 43 | + serverThread = new Thread(() -> { |
| 44 | + try { |
| 45 | + ApplicationContext context = new ApplicationContext(); |
| 46 | + context.setAttribute("--server-port", String.valueOf(TEST_PORT)); |
| 47 | + ApplicationManager.call("start", context, Action.Mode.CLI); |
| 48 | + } catch (Exception e) { |
| 49 | + e.printStackTrace(); |
| 50 | + } |
| 51 | + }); |
| 52 | + serverThread.setDaemon(true); |
| 53 | + serverThread.start(); |
| 54 | + |
| 55 | + // Wait for server to be ready |
| 56 | + boolean started = false; |
| 57 | + for (int i = 0; i < 30; i++) { |
| 58 | + try (Socket socket = new Socket("localhost", TEST_PORT)) { |
| 59 | + started = true; |
| 60 | + break; |
| 61 | + } catch (Exception e) { |
| 62 | + Thread.sleep(1000); |
| 63 | + } |
| 64 | + } |
| 65 | + if (!started) { |
| 66 | + throw new RuntimeException("Server failed to start within 30 seconds"); |
| 67 | + } |
| 68 | + |
| 69 | + // Give server a moment to fully initialize |
| 70 | + Thread.sleep(500); |
| 71 | + } |
| 72 | + |
| 73 | + @AfterAll |
| 74 | + public void tearDown() { |
| 75 | + if (httpServer != null) { |
| 76 | + httpServer.stop(); |
| 77 | + } |
| 78 | + if (serverThread != null && serverThread.isAlive()) { |
| 79 | + serverThread.interrupt(); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + @Test |
| 84 | + public void testActionModeFromName() { |
| 85 | + // Test HTTP method name to Mode mapping |
| 86 | + assertEquals(Action.Mode.HTTP_GET, Action.Mode.fromName("GET")); |
| 87 | + assertEquals(Action.Mode.HTTP_POST, Action.Mode.fromName("POST")); |
| 88 | + assertEquals(Action.Mode.HTTP_PUT, Action.Mode.fromName("PUT")); |
| 89 | + assertEquals(Action.Mode.HTTP_DELETE, Action.Mode.fromName("DELETE")); |
| 90 | + assertEquals(Action.Mode.HTTP_PATCH, Action.Mode.fromName("PATCH")); |
| 91 | + assertEquals(Action.Mode.HTTP_HEAD, Action.Mode.fromName("HEAD")); |
| 92 | + assertEquals(Action.Mode.HTTP_OPTIONS, Action.Mode.fromName("OPTIONS")); |
| 93 | + |
| 94 | + // Test case insensitivity |
| 95 | + assertEquals(Action.Mode.HTTP_GET, Action.Mode.fromName("get")); |
| 96 | + assertEquals(Action.Mode.HTTP_POST, Action.Mode.fromName("post")); |
| 97 | + |
| 98 | + // Test null and unknown values return DEFAULT |
| 99 | + assertEquals(Action.Mode.DEFAULT, Action.Mode.fromName(null)); |
| 100 | + assertEquals(Action.Mode.DEFAULT, Action.Mode.fromName("UNKNOWN")); |
| 101 | + } |
| 102 | + |
| 103 | + @Test |
| 104 | + public void testHttpGetRequest() throws Exception { |
| 105 | + // Make actual HTTP GET request |
| 106 | + String response = sendHttpRequest("GET", BASE_URL + "/?q=api/users", null); |
| 107 | + assertTrue(response.contains("GET users"), "GET request should return 'GET users'"); |
| 108 | + } |
| 109 | + |
| 110 | + @Test |
| 111 | + public void testHttpPostRequest() throws Exception { |
| 112 | + // Make actual HTTP POST request |
| 113 | + String response = sendHttpRequest("POST", BASE_URL + "/?q=api/users", null); |
| 114 | + assertTrue(response.contains("POST users"), "POST request should return 'POST users'"); |
| 115 | + } |
| 116 | + |
| 117 | + @Test |
| 118 | + public void testHttpPutRequest() throws Exception { |
| 119 | + // Make actual HTTP PUT request |
| 120 | + String response = sendHttpRequest("PUT", BASE_URL + "/?q=api/users/123", null); |
| 121 | + assertTrue(response.contains("PUT user"), "PUT request should return 'PUT user'"); |
| 122 | + assertTrue(response.contains("123"), "PUT request should include the ID parameter"); |
| 123 | + } |
| 124 | + |
| 125 | + @Test |
| 126 | + public void testHttpDeleteRequest() throws Exception { |
| 127 | + // Make actual HTTP DELETE request |
| 128 | + String response = sendHttpRequest("DELETE", BASE_URL + "/?q=api/users/456", null); |
| 129 | + assertTrue(response.contains("DELETE user"), "DELETE request should return 'DELETE user'"); |
| 130 | + assertTrue(response.contains("456"), "DELETE request should include the ID parameter"); |
| 131 | + } |
| 132 | + |
| 133 | + @Test |
| 134 | + public void testDefaultModeAcceptsAllMethods() throws Exception { |
| 135 | + // Test that DEFAULT mode actions accept any HTTP method |
| 136 | + String getResponse = sendHttpRequest("GET", BASE_URL + "/?q=api/ping", null); |
| 137 | + assertTrue(getResponse.contains("pong"), "GET request to ping should return 'pong'"); |
| 138 | + |
| 139 | + String postResponse = sendHttpRequest("POST", BASE_URL + "/?q=api/ping", null); |
| 140 | + assertTrue(postResponse.contains("pong"), "POST request to ping should return 'pong'"); |
| 141 | + |
| 142 | + String putResponse = sendHttpRequest("PUT", BASE_URL + "/?q=api/ping", null); |
| 143 | + assertTrue(putResponse.contains("pong"), "PUT request to ping should return 'pong'"); |
| 144 | + } |
| 145 | + |
| 146 | + @Test |
| 147 | + public void testHttpMethodMismatch() throws Exception { |
| 148 | + // Try to access GET endpoint with POST method - should fail or return error |
| 149 | + // Note: This depends on how the server handles method mismatches |
| 150 | + String response = sendHttpRequest("POST", BASE_URL + "/?q=api/users", null); |
| 151 | + // POST to api/users should work (there's a POST handler), but let's verify it's not the GET handler |
| 152 | + assertTrue(response.contains("POST users"), "POST request should match POST handler, not GET"); |
| 153 | + } |
| 154 | + |
| 155 | + /** |
| 156 | + * Helper method to send HTTP requests |
| 157 | + */ |
| 158 | + private String sendHttpRequest(String method, String urlString, String body) throws Exception { |
| 159 | + URLRequest request = new URLRequest(URI.create(urlString).toURL()); |
| 160 | + request.setMethod(method.toUpperCase()); |
| 161 | + |
| 162 | + if (body != null && (method.equalsIgnoreCase("POST") || method.equalsIgnoreCase("PUT"))) { |
| 163 | + request.setHeader("Content-Type", "application/x-www-form-urlencoded"); |
| 164 | + request.setBody(body); |
| 165 | + } |
| 166 | + |
| 167 | + HTTPHandler handler = new HTTPHandler(); |
| 168 | + URLResponse response = handler.handleRequest(request); |
| 169 | + |
| 170 | + int statusCode = response.getStatusCode(); |
| 171 | + String responseText = response.getBody(); |
| 172 | + |
| 173 | + if (statusCode >= 200 && statusCode < 300) { |
| 174 | + return responseText; |
| 175 | + } else { |
| 176 | + // return error response text for non-2xx responses |
| 177 | + return responseText; |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + @Test |
| 182 | + public void testHttpMethodExtractionFromRequest() { |
| 183 | + // Test that HTTP method can be extracted from Method enum |
| 184 | + org.tinystruct.http.Method getMethod = org.tinystruct.http.Method.GET; |
| 185 | + Action.Mode mode = Action.Mode.fromName(getMethod.name()); |
| 186 | + assertEquals(Action.Mode.HTTP_GET, mode); |
| 187 | + |
| 188 | + org.tinystruct.http.Method postMethod = org.tinystruct.http.Method.POST; |
| 189 | + mode = Action.Mode.fromName(postMethod.name()); |
| 190 | + assertEquals(Action.Mode.HTTP_POST, mode); |
| 191 | + } |
| 192 | + |
| 193 | + public class TestWebApp extends AbstractApplication { |
| 194 | + @Override |
| 195 | + public void init() { |
| 196 | + this.setTemplateRequired(false); |
| 197 | + } |
| 198 | + |
| 199 | + @Action( |
| 200 | + value = "api/users", |
| 201 | + description = "Get users", |
| 202 | + mode = Action.Mode.HTTP_GET |
| 203 | + ) |
| 204 | + public String getUsers() { |
| 205 | + return "GET users"; |
| 206 | + } |
| 207 | + |
| 208 | + @Action( |
| 209 | + value = "api/users", |
| 210 | + description = "Create user", |
| 211 | + mode = Action.Mode.HTTP_POST |
| 212 | + ) |
| 213 | + public String createUser() { |
| 214 | + return "POST users"; |
| 215 | + } |
| 216 | + |
| 217 | + @Action( |
| 218 | + value = "api/users", |
| 219 | + description = "Update user", |
| 220 | + mode = Action.Mode.HTTP_PUT |
| 221 | + ) |
| 222 | + public String updateUser(String id) { |
| 223 | + return "PUT user " + (id != null ? id : "unknown"); |
| 224 | + } |
| 225 | + |
| 226 | + @Action( |
| 227 | + value = "api/users", |
| 228 | + description = "Delete user", |
| 229 | + mode = Action.Mode.HTTP_DELETE |
| 230 | + ) |
| 231 | + public String deleteUser(String id) { |
| 232 | + return "DELETE user " + (id != null ? id : "unknown"); |
| 233 | + } |
| 234 | + |
| 235 | + @Action( |
| 236 | + value = "api/ping", |
| 237 | + description = "Ping endpoint", |
| 238 | + mode = Action.Mode.DEFAULT |
| 239 | + ) |
| 240 | + public String ping() { |
| 241 | + return "pong"; |
| 242 | + } |
| 243 | + |
| 244 | + @Override |
| 245 | + public String version() { |
| 246 | + return "test"; |
| 247 | + } |
| 248 | + } |
| 249 | +} |
| 250 | + |
0 commit comments