Add WebSocket support with Jakarta WebSocket integration
- Implemented WebSocketSession abstract class with callback-based API - Added ServletWebSocketSession with full Jakarta WebSocket bridging - Created @WebSocket annotation for declarative endpoint marking - Updated JettyApp to initialize Jakarta WebSocket container - Split Request/Response into abstract base and servlet implementations - Moved JettyApp to jabba.servlet package - Moved annotations to jabba.decor package - Added comprehensive WebSocket test suite (5 tests, all passing) - Updated README.md with WebSocket documentation and examples - All 31 tests passing (async, sync, security, websocket, database) - Fixed spelling errors in README.md
This commit is contained in:
@@ -22,10 +22,10 @@ public class TerminalTest {
|
||||
name="dbo.Maps"
|
||||
)
|
||||
public static class Maps extends DBO{
|
||||
public static Field map_id=Field.Int("map_id").setPk(true);
|
||||
public static Field map_name=Field.Str("map_name");
|
||||
public static Field created=Field.DateTime("created");
|
||||
public static Field active=Field.Bool("active");
|
||||
public static Field map_id=Field.Int("Map_id").setPk(true);
|
||||
public static Field map_name=Field.Str("Map_name");
|
||||
public static Field created=Field.DateTime("Created");
|
||||
public static Field active=Field.Bool("Active");
|
||||
static{
|
||||
//Entity.publish(Maps.class);
|
||||
}
|
||||
|
||||
@@ -12,14 +12,18 @@ public class ArgsConfigTest {
|
||||
ArgsConfig args=new ArgsConfig("prog","--verbose","--key","value","cmd");
|
||||
try {
|
||||
args.load();
|
||||
ArgsConfig.Property<String> env_user=new ArgsConfig.Property<>("USER",String.class);
|
||||
// Cross-platform username check: USER on Unix/Linux/Mac, USERNAME on Windows
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
boolean isWindows = osName.contains("win");
|
||||
ArgsConfig.Property<String> env_user = new ArgsConfig.Property<>(
|
||||
isWindows ? "USERNAME" : "USER", String.class);
|
||||
ArgsConfig.Property<String> sys_user=new ArgsConfig.Property<>("user.name",String.class);
|
||||
ArgsConfig.Property<Boolean> verbose=new ArgsConfig.Property<>("verbose",Boolean.class);
|
||||
String usr_val1=args.getProperty(env_user,"None1");
|
||||
String usr_val2=args.getProperty(sys_user,"None2");
|
||||
System.out.println("Env User:"+usr_val1);
|
||||
System.out.println("Sys User:"+usr_val2);
|
||||
assertTrue(usr_val1.equals(usr_val2));
|
||||
assertTrue("Environment username should match system username", usr_val1.equals(usr_val2));
|
||||
System.out.println("Positional:"+args.getProperty(Config.APP_ARGS, null));
|
||||
System.out.println("Verbose:"+verbose.get(args));
|
||||
for(ArgsConfig.Property<?> p:args){
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
Copyright (c) 2011-2022 Reliancy LLC
|
||||
|
||||
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
|
||||
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||
You may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.reliancy.jabba;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import com.reliancy.jabba.decor.Async;
|
||||
import com.reliancy.jabba.decor.Routed;
|
||||
import com.reliancy.jabba.servlet.JettyApp;
|
||||
|
||||
/**
|
||||
* Test async endpoint support.
|
||||
*/
|
||||
public class AsyncTest {
|
||||
|
||||
public static class TestApp extends JettyApp {
|
||||
@Override
|
||||
public void configure(Config conf) throws Exception {
|
||||
super.configure(conf);
|
||||
// Import routes from this app - router is set by super.configure()
|
||||
Router router = getRouter();
|
||||
if(router != null){
|
||||
router.importMethods(this);
|
||||
router.compile();
|
||||
} else {
|
||||
// Router not set yet, set it ourselves
|
||||
Router newRouter = new Router();
|
||||
newRouter.importMethods(this);
|
||||
newRouter.compile();
|
||||
setRouter(newRouter);
|
||||
}
|
||||
}
|
||||
@Routed(path="/async")
|
||||
public CompletableFuture<String> asyncEndpoint() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
// Simulate long-running operation
|
||||
Thread.sleep(100);
|
||||
return "Async result";
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Routed(path="/sync")
|
||||
public String syncEndpoint() {
|
||||
return "Sync result";
|
||||
}
|
||||
|
||||
@Routed(path="/asyncWithParam")
|
||||
public CompletableFuture<String> asyncWithParam(int delay) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
return "Delayed: " + delay + "ms";
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Routed(path="/asyncAnnotation")
|
||||
@Async
|
||||
public String asyncWithAnnotation(String input, int value) {
|
||||
// Regular method with @Async annotation - should be detected as async
|
||||
return "Processed: " + input + " (" + value + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncEndpointDetection() throws Exception {
|
||||
TestApp app = new TestApp();
|
||||
|
||||
// Test async endpoint directly
|
||||
java.lang.reflect.Method asyncMethod = TestApp.class.getMethod("asyncEndpoint");
|
||||
MethodEndPoint asyncEp = new MethodEndPoint(app, asyncMethod);
|
||||
assertTrue("Endpoint should be detected as async", asyncEp.isAsync());
|
||||
|
||||
// Test sync endpoint directly
|
||||
java.lang.reflect.Method syncMethod = TestApp.class.getMethod("syncEndpoint");
|
||||
MethodEndPoint syncEp = new MethodEndPoint(app, syncMethod);
|
||||
assertFalse("Endpoint should be detected as sync", syncEp.isAsync());
|
||||
|
||||
// Test async with params
|
||||
java.lang.reflect.Method asyncParamMethod = TestApp.class.getMethod("asyncWithParam", int.class);
|
||||
MethodEndPoint asyncParamEp = new MethodEndPoint(app, asyncParamMethod);
|
||||
assertTrue("Endpoint with params should be detected as async", asyncParamEp.isAsync());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompletableFutureReturnType() throws Exception {
|
||||
TestApp app = new TestApp();
|
||||
java.lang.reflect.Method method = TestApp.class.getMethod("asyncEndpoint");
|
||||
MethodEndPoint endpoint = new MethodEndPoint(app, method);
|
||||
|
||||
assertTrue("Should detect CompletableFuture return type", endpoint.isAsync());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncAnnotation() throws Exception {
|
||||
TestApp app = new TestApp();
|
||||
|
||||
// Test method with @Async annotation and regular args/return type
|
||||
java.lang.reflect.Method asyncAnnotMethod = TestApp.class.getMethod("asyncWithAnnotation", String.class, int.class);
|
||||
MethodEndPoint asyncAnnotEp = new MethodEndPoint(app, asyncAnnotMethod);
|
||||
|
||||
// Should be detected as async because of @Async annotation
|
||||
assertTrue("Endpoint with @Async annotation should be detected as async", asyncAnnotEp.isAsync());
|
||||
|
||||
// Verify it has regular return type (not CompletableFuture)
|
||||
assertFalse("Return type should not be CompletableFuture",
|
||||
CompletableFuture.class.isAssignableFrom(asyncAnnotEp.method.getReturnType()));
|
||||
|
||||
// Verify it has regular parameters
|
||||
assertEquals("Should have 2 parameters", 2, asyncAnnotEp.method.getParameterCount());
|
||||
}
|
||||
|
||||
private TestApp app;
|
||||
private int testPort;
|
||||
private String baseUrl;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// Use a random port to avoid conflicts
|
||||
testPort = 18090 + (int)(Math.random() * 1000);
|
||||
baseUrl = "http://localhost:" + testPort;
|
||||
|
||||
app = new TestApp();
|
||||
ArgsConfig config = new ArgsConfig();
|
||||
Config.SERVER_PORT.set(config, testPort);
|
||||
config.load();
|
||||
app.begin(config);
|
||||
|
||||
// Wait for server to be started (not necessarily running, which requires work() to be called)
|
||||
int attempts = 0;
|
||||
while(!app.isStarted() && attempts < 20){
|
||||
Thread.sleep(100);
|
||||
attempts++;
|
||||
}
|
||||
if(!app.isStarted()){
|
||||
throw new Exception("Server failed to start on port " + testPort);
|
||||
}
|
||||
// Give server a moment to be ready
|
||||
Thread.sleep(200);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
if(app != null){
|
||||
try {
|
||||
if(app.isRunning()){
|
||||
app.end();
|
||||
// Give server a moment to stop
|
||||
Thread.sleep(300);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
app = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to make HTTP GET request
|
||||
*/
|
||||
private String httpGet(String path) throws Exception {
|
||||
URL url = new URL(baseUrl + path);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(15000); // Longer timeout for async operations
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if(responseCode == HttpURLConnection.HTTP_OK){
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while((line = in.readLine()) != null){
|
||||
response.append(line);
|
||||
}
|
||||
in.close();
|
||||
return response.toString();
|
||||
}else{
|
||||
// Read error stream for more info
|
||||
String errorMsg = "HTTP request failed with code: " + responseCode;
|
||||
try {
|
||||
BufferedReader err = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
|
||||
String errLine;
|
||||
while((errLine = err.readLine()) != null){
|
||||
errorMsg += "\n" + errLine;
|
||||
}
|
||||
err.close();
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
throw new Exception(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSyncEndpointIntegration() throws Exception {
|
||||
// Test synchronous endpoint first to verify basic connectivity
|
||||
String result = httpGet("/sync");
|
||||
assertEquals("Sync endpoint should return correct result", "Sync result", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncEndpointIntegration() throws Exception {
|
||||
// Test CompletableFuture return type endpoint
|
||||
long startTime = System.currentTimeMillis();
|
||||
String result = httpGet("/async");
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
assertEquals("Async endpoint should return correct result", "Async result", result);
|
||||
// Should take at least 100ms (the sleep time in the endpoint)
|
||||
assertTrue("Async endpoint should take time", duration >= 90);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAsyncWithParamIntegration() throws Exception {
|
||||
// Test async endpoint with parameters
|
||||
long startTime = System.currentTimeMillis();
|
||||
String result = httpGet("/asyncWithParam?delay=50");
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
assertTrue("Result should contain delay info", result.contains("Delayed: 50ms"));
|
||||
// Should take at least 50ms
|
||||
assertTrue("Async endpoint with delay should take time", duration >= 40);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncAnnotationIntegration() throws Exception {
|
||||
// Test @Async annotation endpoint
|
||||
String result = httpGet("/asyncAnnotation?input=test&value=42");
|
||||
|
||||
assertEquals("Async annotation endpoint should return correct result",
|
||||
"Processed: test (42)", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncNonBlocking() throws Exception {
|
||||
// Test that async endpoints don't block the server
|
||||
// First verify the endpoint works with a single request
|
||||
String singleResult = httpGet("/async");
|
||||
assertEquals("Single async request should work", "Async result", singleResult);
|
||||
|
||||
// Make multiple concurrent requests
|
||||
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return httpGet("/async");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return "ERROR: " + e.getMessage();
|
||||
}
|
||||
});
|
||||
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return httpGet("/async");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return "ERROR: " + e.getMessage();
|
||||
}
|
||||
});
|
||||
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return httpGet("/async");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return "ERROR: " + e.getMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all to complete
|
||||
CompletableFuture.allOf(future1, future2, future3).join();
|
||||
|
||||
// All should succeed
|
||||
String result1 = future1.get();
|
||||
String result2 = future2.get();
|
||||
String result3 = future3.get();
|
||||
|
||||
assertEquals("First request should succeed", "Async result", result1);
|
||||
assertEquals("Second request should succeed", "Async result", result2);
|
||||
assertEquals("Third request should succeed", "Async result", result3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.reliancy.jabba.decor.Routed;
|
||||
import com.reliancy.jabba.sec.NotAuthentic;
|
||||
import com.reliancy.jabba.sec.Secured;
|
||||
import com.reliancy.jabba.servlet.JettyApp;
|
||||
import com.reliancy.jabba.sec.SecurityActor;
|
||||
import com.reliancy.jabba.sec.SecurityPolicy;
|
||||
import com.reliancy.jabba.sec.plain.PlainSecurityStore;
|
||||
@@ -22,7 +24,10 @@ import com.reliancy.util.Resources;
|
||||
*/
|
||||
public class DemoApp extends JettyApp implements AppModule{
|
||||
public static void main( String[] args ) throws Exception{
|
||||
Config cnf=new ArgsConfig(args).load();
|
||||
ArgsConfig cnf=new ArgsConfig(args);
|
||||
cnf.setProperty(Config.SERVER_PORT,8088);
|
||||
cnf.setProperty(Config.LOG_LEVEL,"DEBUG"); // Set BEFORE load()
|
||||
cnf.load();
|
||||
JettyApp app=new DemoApp();
|
||||
app.run(cnf);
|
||||
}
|
||||
@@ -57,11 +62,13 @@ public class DemoApp extends JettyApp implements AppModule{
|
||||
// install file sever endpoint
|
||||
FileServer fs=new FileServer("/static","/public");
|
||||
fs.publish(app);
|
||||
// publish DemoApp's own routes
|
||||
this.publish(app);
|
||||
Menu top_menu=Menu.request(Menu.TOP);
|
||||
top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login"));
|
||||
top_menu.setTitle("Jabba3");
|
||||
app.getRouter().compile();
|
||||
System.out.println(app.getRouter().regex);
|
||||
log().debug("Router regex:{}",app.getRouter().regex);
|
||||
}
|
||||
@Override
|
||||
public void publish(App app) {
|
||||
@@ -74,7 +81,7 @@ public class DemoApp extends JettyApp implements AppModule{
|
||||
String ret="";
|
||||
try {
|
||||
Template t=Template.find("/templates/login.hbs");
|
||||
System.out.println("Template:"+t);
|
||||
log().debug("Template:{}",t);
|
||||
ret = t.render(context).toString();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -127,11 +134,10 @@ public class DemoApp extends JettyApp implements AppModule{
|
||||
// here we need to process login and redirect
|
||||
AppSession ass=AppSession.getInstance();
|
||||
try{
|
||||
System.out.println("Post login");
|
||||
log().debug("Post login");
|
||||
String userid=(String)req.getParam("userid",null);
|
||||
String pwd=(String)req.getParam("password",null);
|
||||
System.out.println("SS:"+ass);
|
||||
System.out.println("P:"+userid+"/"+pwd);
|
||||
log().debug("Session:{}",ass);
|
||||
SecurityPolicy secpol=ass.getApp().getSecurityPolicy();
|
||||
SecurityActor user=secpol.authenticate(userid, pwd);
|
||||
if(user==null) throw new NotAuthentic("invalid credentials");
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
Copyright (c) 2011-2022 Reliancy LLC
|
||||
|
||||
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
|
||||
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||
You may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.reliancy.jabba;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import com.reliancy.jabba.decor.Routed;
|
||||
import com.reliancy.jabba.servlet.JettyApp;
|
||||
|
||||
/**
|
||||
* Integration tests for JettyApp regular (non-async) functionality.
|
||||
*/
|
||||
public class JettyAppTest {
|
||||
|
||||
public static class SimpleTestApp extends JettyApp implements AppModule {
|
||||
@Override
|
||||
public void configure(Config conf) throws Exception {
|
||||
super.configure(conf);
|
||||
// Set up router and import methods
|
||||
Router router = getRouter();
|
||||
if(router == null){
|
||||
router = new Router();
|
||||
setRouter(router);
|
||||
}
|
||||
router.importMethods(this);
|
||||
router.compile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(App app) {
|
||||
app.getRouter().importMethods(this);
|
||||
}
|
||||
|
||||
@Routed(path="/test")
|
||||
public String test() {
|
||||
return "test response";
|
||||
}
|
||||
|
||||
@Routed(path="/testPlain")
|
||||
public void testPlain(Request req, Response resp) throws java.io.IOException {
|
||||
resp.getEncoder().writeln("plain response");
|
||||
}
|
||||
|
||||
@Routed(path="/testParam/{id:int}")
|
||||
public String testParam(int id) {
|
||||
return "param: " + id;
|
||||
}
|
||||
|
||||
@Routed(path="/testQuery")
|
||||
public String testQuery(String name) {
|
||||
return "query: " + name;
|
||||
}
|
||||
|
||||
@Routed(path="/testNoArg")
|
||||
public String testNoArg() {
|
||||
return "no arg response";
|
||||
}
|
||||
}
|
||||
|
||||
private SimpleTestApp app;
|
||||
private int testPort;
|
||||
private String baseUrl;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// Use a random port to avoid conflicts
|
||||
testPort = 18090 + (int)(Math.random() * 1000);
|
||||
baseUrl = "http://localhost:" + testPort;
|
||||
|
||||
app = new SimpleTestApp();
|
||||
ArgsConfig config = new ArgsConfig();
|
||||
Config.SERVER_PORT.set(config, testPort);
|
||||
config.load();
|
||||
app.begin(config);
|
||||
|
||||
// Wait for server to be started
|
||||
int attempts = 0;
|
||||
while(!app.isStarted() && attempts < 20){
|
||||
Thread.sleep(100);
|
||||
attempts++;
|
||||
}
|
||||
if(!app.isStarted()){
|
||||
throw new Exception("Server failed to start on port " + testPort);
|
||||
}
|
||||
// Give server a moment to be ready
|
||||
Thread.sleep(200);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
if(app != null){
|
||||
try {
|
||||
if(app.isStarted()){
|
||||
app.end();
|
||||
// Give server a moment to stop
|
||||
Thread.sleep(300);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
app = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to make HTTP GET request
|
||||
*/
|
||||
private String httpGet(String path) throws Exception {
|
||||
URL url = new URL(baseUrl + path);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if(responseCode == HttpURLConnection.HTTP_OK){
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while((line = in.readLine()) != null){
|
||||
response.append(line);
|
||||
}
|
||||
in.close();
|
||||
return response.toString();
|
||||
}else{
|
||||
// Read error stream for more info
|
||||
String errorMsg = "HTTP request failed with code: " + responseCode;
|
||||
try {
|
||||
BufferedReader err = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
|
||||
String errLine;
|
||||
while((errLine = err.readLine()) != null){
|
||||
errorMsg += "\n" + errLine;
|
||||
}
|
||||
err.close();
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
throw new Exception(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleStringReturn() throws Exception {
|
||||
String result = httpGet("/test");
|
||||
assertEquals("Simple string return should work", "test response", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainRequestResponse() throws Exception {
|
||||
String result = httpGet("/testPlain");
|
||||
assertTrue("Plain request/response should work", result.contains("plain response"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathParameter() throws Exception {
|
||||
String result = httpGet("/testParam/42");
|
||||
assertEquals("Path parameter should work", "param: 42", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryParameter() throws Exception {
|
||||
String result = httpGet("/testQuery?name=testvalue");
|
||||
assertEquals("Query parameter should work", "query: testvalue", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoArgMethod() throws Exception {
|
||||
String result = httpGet("/testNoArg");
|
||||
assertEquals("No-arg method should work", "no arg response", result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.reliancy.jabba.decor.Routed;
|
||||
import com.reliancy.jabba.servlet.JettyApp;
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
Copyright (c) 2011-2022 Reliancy LLC
|
||||
|
||||
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
|
||||
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||
You may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.reliancy.jabba;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.reliancy.jabba.decor.Routed;
|
||||
import com.reliancy.jabba.sec.SecurityPolicy;
|
||||
import com.reliancy.jabba.sec.plain.PlainSecurityStore;
|
||||
import com.reliancy.util.Handy;
|
||||
|
||||
/**
|
||||
* Security tests for authentication and routing.
|
||||
*/
|
||||
public class SecurityTest {
|
||||
|
||||
/** Minimal test implementation of Response for testing */
|
||||
static class TestResponse extends Response {
|
||||
private int status = 200;
|
||||
|
||||
public TestResponse(Request request) {
|
||||
super(request);
|
||||
}
|
||||
@Override public void setContentType(String type) {}
|
||||
@Override public void setStatus(int status) { this.status = status; }
|
||||
@Override public String getHeader(String name) {
|
||||
for(HTTP.Header header : headers) {
|
||||
if(header.key.equalsIgnoreCase(name)) return header.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override public Response setHeader(String name, String value) {
|
||||
headers.add(new HTTP.Header(name.toLowerCase(), value));
|
||||
return this;
|
||||
}
|
||||
public String getCookie(String name) {
|
||||
for(HTTP.Cookie cookie : cookies) {
|
||||
if(cookie.key.equals(name)) return cookie.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override public Response setCookie(String name, String value, int maxAge, boolean secure) {
|
||||
cookies.add(new HTTP.Cookie(name, value, maxAge, secure, false));
|
||||
return this;
|
||||
}
|
||||
@Override public boolean isCommitted() { return false; }
|
||||
@Override public void commit() {}
|
||||
@Override public boolean isCompleted() { return false; }
|
||||
@Override public void complete() {}
|
||||
@Override public java.io.OutputStream getOutputStream() throws IOException { return null; }
|
||||
@Override public java.io.Writer getWriter() throws IOException { return null; }
|
||||
@Override public com.reliancy.jabba.WebSocketSession upgradeToWebSocket(String route, com.reliancy.jabba.Session appSession) throws IOException {
|
||||
throw new UnsupportedOperationException("WebSocket not supported in test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecretKeyFromEnvironment() throws Exception {
|
||||
// Test that secret key can be loaded from environment using reflection
|
||||
String originalKey = System.getenv("JABBA_SECRET_KEY");
|
||||
try {
|
||||
System.setProperty("jabba.secret.key", "test-secret-key-12345");
|
||||
SecurityPolicy policy = new SecurityPolicy();
|
||||
java.lang.reflect.Method getSecretMethod = SecurityPolicy.class.getDeclaredMethod("getSecret");
|
||||
getSecretMethod.setAccessible(true);
|
||||
String secret = (String) getSecretMethod.invoke(policy);
|
||||
assertNotNull("Secret should not be null", secret);
|
||||
assertFalse("Secret should not be empty", secret.isEmpty());
|
||||
} finally {
|
||||
if (originalKey != null) {
|
||||
System.setProperty("jabba.secret.key", originalKey);
|
||||
} else {
|
||||
System.clearProperty("jabba.secret.key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAESEncryption() {
|
||||
// Test AES encryption/decryption
|
||||
String key = "test-secret-key-for-encryption-12345678901234567890";
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("user", "testuser");
|
||||
data.put("pass", "testpass");
|
||||
|
||||
String encrypted = Handy.encrypt(key, data);
|
||||
assertNotNull("Encrypted data should not be null", encrypted);
|
||||
assertFalse("Encrypted data should not be empty", encrypted.isEmpty());
|
||||
|
||||
Map<String, String> decrypted = Handy.decrypt(key, encrypted);
|
||||
assertEquals("Decrypted user should match", "testuser", decrypted.get("user"));
|
||||
assertEquals("Decrypted pass should match", "testpass", decrypted.get("pass"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInputValidation() throws Exception {
|
||||
// Test input validation in MethodEndPoint using reflection to access protected method
|
||||
MethodEndPoint endpoint = new MethodEndPoint(new TestEndpoint(),
|
||||
TestEndpoint.class.getMethod("testMethod", String.class));
|
||||
|
||||
java.lang.reflect.Method validateMethod = MethodEndPoint.class.getDeclaredMethod(
|
||||
"validateInput", Object.class, Class.class, String.class);
|
||||
validateMethod.setAccessible(true);
|
||||
|
||||
// Test normal input
|
||||
Object valid = validateMethod.invoke(endpoint, "normal string", String.class, "testParam");
|
||||
assertEquals("Normal string should pass validation", "normal string", valid);
|
||||
|
||||
// Test null input
|
||||
Object nullVal = validateMethod.invoke(endpoint, null, String.class, "testParam");
|
||||
assertNull("Null input should return null", nullVal);
|
||||
|
||||
// Test very long string (should be truncated)
|
||||
StringBuilder longStr = new StringBuilder();
|
||||
for (int i = 0; i < 100001; i++) {
|
||||
longStr.append("a");
|
||||
}
|
||||
Object longInput = validateMethod.invoke(endpoint, longStr.toString(), String.class, "testParam");
|
||||
assertNotNull("Long input should not be null", longInput);
|
||||
assertTrue("Long input should be truncated", ((String)longInput).length() <= 100000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCookieSecurity() throws IOException {
|
||||
// Test that cookies are set with HttpOnly flag
|
||||
Response response = new TestResponse((Request)null);
|
||||
response.setCookie("test", "value", 3600, true, true);
|
||||
|
||||
// Verify cookie was added
|
||||
assertNotNull("Cookie should be added", response.getCookie("test"));
|
||||
assertEquals("Cookie value should match", "value", response.getCookie("test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseHeaderLookup() {
|
||||
// Test that header lookup works correctly (bug fix verification)
|
||||
Response response = new TestResponse((Request)null);
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
|
||||
String header = response.getHeader("content-type");
|
||||
assertEquals("Header lookup should be case-insensitive", "application/json", header);
|
||||
}
|
||||
|
||||
// Test endpoint class for testing
|
||||
public static class TestEndpoint {
|
||||
@Routed
|
||||
public String testMethod(String param) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
Copyright (c) 2011-2022 Reliancy LLC
|
||||
|
||||
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
|
||||
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||
You may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.reliancy.jabba;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.Session.Listener;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import com.reliancy.jabba.decor.Routed;
|
||||
import com.reliancy.jabba.decor.WebSocket;
|
||||
import com.reliancy.jabba.servlet.JettyApp;
|
||||
|
||||
/**
|
||||
* Integration tests for WebSocket functionality.
|
||||
* Tests the new WebSocket architecture using @WebSocket + @Routed annotations
|
||||
* and WebSocketSession argument-based endpoints.
|
||||
*/
|
||||
public class WebSocketTest {
|
||||
|
||||
/**
|
||||
* Test application with WebSocket endpoints using new API.
|
||||
*/
|
||||
public static class TestWebSocketApp extends JettyApp {
|
||||
private int messageCount = 0;
|
||||
|
||||
public TestWebSocketApp() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Config conf) throws Exception {
|
||||
super.configure(conf);
|
||||
// Import methods from this class to the router
|
||||
Router router = getRouter();
|
||||
if(router != null) {
|
||||
router.importMethods(this);
|
||||
router.compile();
|
||||
}
|
||||
}
|
||||
|
||||
// Echo endpoint - sends back what it receives
|
||||
@Routed(path="/ws/echo")
|
||||
@WebSocket
|
||||
public void echoEndpoint(com.reliancy.jabba.WebSocketSession session) {
|
||||
session.onText(msg -> {
|
||||
try {
|
||||
session.sendText("Echo: " + msg);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Simple endpoint that counts messages
|
||||
@Routed(path="/ws/counter")
|
||||
@WebSocket
|
||||
public void counterEndpoint(com.reliancy.jabba.WebSocketSession session) {
|
||||
session.onText(msg -> {
|
||||
try {
|
||||
messageCount++;
|
||||
session.sendText("Message #" + messageCount);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Endpoint with immediate response on connect
|
||||
@Routed(path="/ws/session")
|
||||
@WebSocket
|
||||
public void sessionEndpoint(com.reliancy.jabba.WebSocketSession session) {
|
||||
session.onText(msg -> {
|
||||
try {
|
||||
session.sendText("Connected: " + session.getId());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// HTTP endpoint for comparison
|
||||
@Routed(path="/test")
|
||||
public String testHttp() {
|
||||
return "HTTP works";
|
||||
}
|
||||
|
||||
public int getMessageCount() {
|
||||
return messageCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple WebSocket client for testing (using Jetty 12 Session.Listener API).
|
||||
*/
|
||||
public static class TestWebSocketClient implements Session.Listener.AutoDemanding {
|
||||
private final BlockingQueue<String> messages = new LinkedBlockingQueue<>();
|
||||
private final CompletableFuture<Session> connectFuture = new CompletableFuture<>();
|
||||
private final CompletableFuture<Void> closeFuture = new CompletableFuture<>();
|
||||
private Session session;
|
||||
|
||||
@Override
|
||||
public void onWebSocketOpen(Session session) {
|
||||
this.session = session;
|
||||
connectFuture.complete(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(String message) {
|
||||
messages.add(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, String reason) {
|
||||
closeFuture.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(Throwable cause) {
|
||||
cause.printStackTrace();
|
||||
}
|
||||
|
||||
public void send(String message) throws Exception {
|
||||
if (session != null && session.isOpen()) {
|
||||
// Jetty 12 API: Session.sendText() directly
|
||||
session.sendText(message, null);
|
||||
}
|
||||
}
|
||||
|
||||
public String receiveMessage(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
return messages.poll(timeout, unit);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (session != null) {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<Session> getConnectFuture() {
|
||||
return connectFuture;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> getCloseFuture() {
|
||||
return closeFuture;
|
||||
}
|
||||
}
|
||||
|
||||
private TestWebSocketApp app;
|
||||
private WebSocketClient wsClient;
|
||||
private int testPort;
|
||||
private String baseWsUrl;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// Use a random port to avoid conflicts
|
||||
testPort = 18090 + (int)(Math.random() * 1000);
|
||||
baseWsUrl = "ws://localhost:" + testPort;
|
||||
|
||||
// Start test app
|
||||
app = new TestWebSocketApp();
|
||||
ArgsConfig config = new ArgsConfig();
|
||||
Config.SERVER_PORT.set(config, testPort);
|
||||
config.load();
|
||||
app.begin(config);
|
||||
|
||||
// Wait for server to start
|
||||
int attempts = 0;
|
||||
while(!app.isStarted() && attempts < 20){
|
||||
Thread.sleep(100);
|
||||
attempts++;
|
||||
}
|
||||
if(!app.isStarted()){
|
||||
throw new Exception("Server failed to start on port " + testPort);
|
||||
}
|
||||
Thread.sleep(200);
|
||||
|
||||
// Create WebSocket client
|
||||
wsClient = new WebSocketClient();
|
||||
wsClient.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
if (wsClient != null) {
|
||||
try {
|
||||
wsClient.stop();
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
if(app != null){
|
||||
try {
|
||||
if(app.isStarted()){
|
||||
app.end();
|
||||
Thread.sleep(300);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
app = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebSocketEchoEndpoint() throws Exception {
|
||||
TestWebSocketClient client = new TestWebSocketClient();
|
||||
|
||||
// Connect to echo endpoint
|
||||
URI uri = new URI(baseWsUrl + "/ws/echo");
|
||||
wsClient.connect(client, uri);
|
||||
|
||||
// Wait for connection
|
||||
Session session = client.getConnectFuture().get(5, TimeUnit.SECONDS);
|
||||
assertNotNull("Connection should be established", session);
|
||||
assertTrue("Session should be open", session.isOpen());
|
||||
|
||||
// Send a message
|
||||
client.send("Hello WebSocket");
|
||||
|
||||
// Receive echo response
|
||||
String response = client.receiveMessage(5, TimeUnit.SECONDS);
|
||||
assertNotNull("Should receive response", response);
|
||||
assertEquals("Should echo back message", "Echo: Hello WebSocket", response);
|
||||
|
||||
// Send another message
|
||||
client.send("Test 123");
|
||||
response = client.receiveMessage(5, TimeUnit.SECONDS);
|
||||
assertEquals("Should echo second message", "Echo: Test 123", response);
|
||||
|
||||
// Close connection
|
||||
client.close();
|
||||
client.getCloseFuture().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebSocketCounterEndpoint() throws Exception {
|
||||
TestWebSocketClient client = new TestWebSocketClient();
|
||||
|
||||
// Connect to counter endpoint
|
||||
URI uri = new URI(baseWsUrl + "/ws/counter");
|
||||
wsClient.connect(client, uri);
|
||||
|
||||
// Wait for connection
|
||||
Session session = client.getConnectFuture().get(5, TimeUnit.SECONDS);
|
||||
assertNotNull("Connection should be established", session);
|
||||
|
||||
// Send multiple messages
|
||||
client.send("msg1");
|
||||
String response1 = client.receiveMessage(5, TimeUnit.SECONDS);
|
||||
assertTrue("First response should contain counter", response1.contains("Message #"));
|
||||
|
||||
client.send("msg2");
|
||||
String response2 = client.receiveMessage(5, TimeUnit.SECONDS);
|
||||
assertTrue("Second response should contain counter", response2.contains("Message #"));
|
||||
|
||||
// Counter should have incremented
|
||||
assertNotEquals("Responses should be different", response1, response2);
|
||||
|
||||
// Close connection
|
||||
client.close();
|
||||
client.getCloseFuture().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebSocketSessionEndpoint() throws Exception {
|
||||
TestWebSocketClient client = new TestWebSocketClient();
|
||||
|
||||
// Connect to session endpoint
|
||||
URI uri = new URI(baseWsUrl + "/ws/session");
|
||||
wsClient.connect(client, uri);
|
||||
|
||||
// Wait for connection
|
||||
Session session = client.getConnectFuture().get(5, TimeUnit.SECONDS);
|
||||
assertNotNull("Connection should be established", session);
|
||||
|
||||
// Send a message to trigger the response
|
||||
client.send("ping");
|
||||
|
||||
// Should receive message with session ID
|
||||
String response = client.receiveMessage(5, TimeUnit.SECONDS);
|
||||
assertNotNull("Should receive connection message", response);
|
||||
assertTrue("Message should contain 'Connected'", response.startsWith("Connected:"));
|
||||
|
||||
// Close connection
|
||||
client.close();
|
||||
client.getCloseFuture().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleWebSocketClients() throws Exception {
|
||||
TestWebSocketClient client1 = new TestWebSocketClient();
|
||||
TestWebSocketClient client2 = new TestWebSocketClient();
|
||||
|
||||
// Connect both clients to echo endpoint
|
||||
URI uri = new URI(baseWsUrl + "/ws/echo");
|
||||
wsClient.connect(client1, uri);
|
||||
wsClient.connect(client2, uri);
|
||||
|
||||
// Wait for connections
|
||||
Session session1 = client1.getConnectFuture().get(5, TimeUnit.SECONDS);
|
||||
Session session2 = client2.getConnectFuture().get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertNotNull("Client 1 should connect", session1);
|
||||
assertNotNull("Client 2 should connect", session2);
|
||||
assertTrue("Client 1 session should be open", session1.isOpen());
|
||||
assertTrue("Client 2 session should be open", session2.isOpen());
|
||||
|
||||
// Send messages from both clients
|
||||
client1.send("From Client 1");
|
||||
client2.send("From Client 2");
|
||||
|
||||
// Receive responses
|
||||
String response1 = client1.receiveMessage(5, TimeUnit.SECONDS);
|
||||
String response2 = client2.receiveMessage(5, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals("Client 1 should receive its echo", "Echo: From Client 1", response1);
|
||||
assertEquals("Client 2 should receive its echo", "Echo: From Client 2", response2);
|
||||
|
||||
// Close connections
|
||||
client1.close();
|
||||
client2.close();
|
||||
client1.getCloseFuture().get(5, TimeUnit.SECONDS);
|
||||
client2.getCloseFuture().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpStillWorksWithWebSocket() throws Exception {
|
||||
// Verify HTTP endpoints still work when WebSocket is enabled
|
||||
java.net.URL url = new java.net.URL("http://localhost:" + testPort + "/test");
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
assertEquals("HTTP endpoint should work", 200, responseCode);
|
||||
|
||||
java.io.BufferedReader in = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(conn.getInputStream()));
|
||||
String response = in.readLine();
|
||||
in.close();
|
||||
|
||||
assertEquals("HTTP response should be correct", "HTTP works", response);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user