|
23 | 23 | import org.junit.jupiter.api.Test; |
24 | 24 |
|
25 | 25 | import java.io.IOException; |
| 26 | +import java.io.InputStream; |
26 | 27 | import java.net.HttpURLConnection; |
27 | 28 | import java.net.URL; |
| 29 | +import java.nio.charset.StandardCharsets; |
28 | 30 | import java.util.Base64; |
29 | 31 | import java.util.HashMap; |
30 | 32 | import java.util.logging.Level; |
@@ -394,4 +396,87 @@ void tokenWithTransaction() throws Exception { |
394 | 396 | } |
395 | 397 | }); |
396 | 398 | } |
| 399 | + |
| 400 | + /** |
| 401 | + * Regression test for <a href="https://github.com/ArcadeData/arcadedb/issues/4082">issue #4082</a>: |
| 402 | + * after a server restart, all in-memory auth sessions are lost. A client that still holds the |
| 403 | + * previous Bearer token must receive HTTP 401 with a JSON body that Studio can react to by |
| 404 | + * resetting the session and showing the login page. |
| 405 | + * <p> |
| 406 | + * Here we simulate the restart by explicitly removing the session from the manager. Studio's |
| 407 | + * global ajaxError handler relies on this 401 to detect the dropped session and prompt the user |
| 408 | + * to log in again, instead of leaving the UI stuck on stale data. |
| 409 | + */ |
| 410 | + @Test |
| 411 | + void invalidTokenAfterServerRestartReturns401() throws Exception { |
| 412 | + testEachServer((serverIndex) -> { |
| 413 | + // 1. LOGIN |
| 414 | + HttpURLConnection connection = (HttpURLConnection) new URL( |
| 415 | + "http://127.0.0.1:248" + serverIndex + "/api/v1/login").openConnection(); |
| 416 | + connection.setRequestMethod("POST"); |
| 417 | + connection.setRequestProperty("Authorization", |
| 418 | + "Basic " + Base64.getEncoder().encodeToString(("root:" + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS).getBytes())); |
| 419 | + connection.connect(); |
| 420 | + |
| 421 | + final String authToken; |
| 422 | + try { |
| 423 | + final String response = readResponse(connection); |
| 424 | + assertThat(connection.getResponseCode()).isEqualTo(200); |
| 425 | + authToken = new JSONObject(response).getString("token"); |
| 426 | + } finally { |
| 427 | + connection.disconnect(); |
| 428 | + } |
| 429 | + |
| 430 | + // 2. SANITY CHECK: Token is valid right after login |
| 431 | + connection = (HttpURLConnection) new URL( |
| 432 | + "http://127.0.0.1:248" + serverIndex + "/api/v1/databases").openConnection(); |
| 433 | + connection.setRequestMethod("GET"); |
| 434 | + connection.setRequestProperty("Authorization", "Bearer " + authToken); |
| 435 | + connection.connect(); |
| 436 | + try { |
| 437 | + assertThat(connection.getResponseCode()).isEqualTo(200); |
| 438 | + } finally { |
| 439 | + connection.disconnect(); |
| 440 | + } |
| 441 | + |
| 442 | + // 3. SIMULATE SERVER RESTART: drop the session from the in-memory session manager |
| 443 | + // (a real restart would reinitialize the manager and lose all sessions the same way). |
| 444 | + getServer(serverIndex).getHttpServer().getAuthSessionManager().removeSession(authToken); |
| 445 | + |
| 446 | + // 4. CLIENT STILL USES THE OLD TOKEN -> server must return 401 with a parseable JSON body |
| 447 | + connection = (HttpURLConnection) new URL( |
| 448 | + "http://127.0.0.1:248" + serverIndex + "/api/v1/databases").openConnection(); |
| 449 | + connection.setRequestMethod("GET"); |
| 450 | + connection.setRequestProperty("Authorization", "Bearer " + authToken); |
| 451 | + connection.connect(); |
| 452 | + |
| 453 | + try { |
| 454 | + assertThat(connection.getResponseCode()).isEqualTo(401); |
| 455 | + // Read from the error stream because the status is 4xx |
| 456 | + final InputStream err = connection.getErrorStream(); |
| 457 | + assertThat(err).isNotNull(); |
| 458 | + final String body = new String(err.readAllBytes(), StandardCharsets.UTF_8); |
| 459 | + assertThat(body).isNotBlank(); |
| 460 | + // Body must be JSON with an "error" key so Studio's globalNotifyError can parse it |
| 461 | + final JSONObject json = new JSONObject(body); |
| 462 | + assertThat(json.has("error")).isTrue(); |
| 463 | + assertThat(json.getString("error")).contains("Invalid or expired authentication token"); |
| 464 | + } finally { |
| 465 | + connection.disconnect(); |
| 466 | + } |
| 467 | + |
| 468 | + // 5. SAME TOKEN AGAINST OTHER ENDPOINTS: must consistently return 401 so the Studio's |
| 469 | + // global handler kicks in regardless of which call was the one to hit the dead session. |
| 470 | + connection = (HttpURLConnection) new URL( |
| 471 | + "http://127.0.0.1:248" + serverIndex + "/api/v1/query/" + DATABASE_NAME + "/sql/select%201").openConnection(); |
| 472 | + connection.setRequestMethod("GET"); |
| 473 | + connection.setRequestProperty("Authorization", "Bearer " + authToken); |
| 474 | + connection.connect(); |
| 475 | + try { |
| 476 | + assertThat(connection.getResponseCode()).isEqualTo(401); |
| 477 | + } finally { |
| 478 | + connection.disconnect(); |
| 479 | + } |
| 480 | + }); |
| 481 | + } |
397 | 482 | } |
0 commit comments