diff --git a/jooby/1-7-easymock-migration.md b/jooby/1-7-easymock-migration.md deleted file mode 100644 index eb07ecb6..00000000 --- a/jooby/1-7-easymock-migration.md +++ /dev/null @@ -1,161 +0,0 @@ -# Phase 1.7 — EasyMock + PowerMock → Mockito Migration - -> This document tracks the migration of all test files from EasyMock+PowerMock to pure Mockito 5. -> After migration, `easymock` and all PowerMock references are removed — only `mockito-core` remains. - ---- - -## Background - -- 76 test files in `src/test/java-excluded/` depend on EasyMock/PowerMock/external HTTP clients. -- `MockUnit.java` is the central test utility — wraps EasyMock's record-replay lifecycle and PowerMock's static/constructor mocking. -- Mockito 5.3.1 (managed by `killbill-oss-parent`) provides `mockStatic()` and `mockConstruction()` natively. - -## Migration Strategy - -Replace the EasyMock record-replay pattern: -```java -// BEFORE (EasyMock + PowerMock) -EasyMock.expect(mock.foo()).andReturn(value); -EasyMock.replay(mock); -// ... test code ... -EasyMock.verify(mock); -``` - -With Mockito's stubbing pattern: -```java -// AFTER (Mockito) -when(mock.foo()).thenReturn(value); -// ... test code ... -verify(mock).foo(); -``` - -Key API mappings: - -| EasyMock / PowerMock | Mockito 5 | -|---|---| -| `EasyMock.createMock(Foo.class)` | `Mockito.mock(Foo.class)` | -| `EasyMock.expect(mock.foo()).andReturn(val)` | `when(mock.foo()).thenReturn(val)` | -| `EasyMock.expect(mock.foo()).andThrow(ex)` | `when(mock.foo()).thenThrow(ex)` | -| `EasyMock.expectLastCall()` | `doNothing().when(mock).foo()` or just call the void method | -| `EasyMock.expectLastCall().andThrow(ex)` | `doThrow(ex).when(mock).foo()` | -| `EasyMock.replay(mock)` | *(not needed — stubs are active immediately)* | -| `EasyMock.verify(mock)` | `verify(mock).foo()` *(per-method, or omit if not needed)* | -| `EasyMock.capture()` | `ArgumentCaptor.forClass(Foo.class)` | -| `EasyMock.isA(Foo.class)` | `any(Foo.class)` | -| `EasyMock.eq(val)` | `eq(val)` | -| `EasyMock.anyObject()` | `any()` | -| `PowerMock.mockStatic(Foo.class)` | `Mockito.mockStatic(Foo.class)` (try-with-resources) | -| `PowerMock.createMockAndExpectNew(Foo.class, args)` | `Mockito.mockConstruction(Foo.class)` | -| `@RunWith(PowerMockRunner.class)` | Remove (or `@ExtendWith(MockitoExtension.class)`) | -| `@PrepareForTest({Foo.class})` | Remove | - ---- - -## Sub-Phases - -### 1.7.1 — Rewrite MockUnit.java ✅ - -- **DONE.** `src/test/java/org/jooby/test/MockUnit.java` rewritten to pure Mockito 5. -- Key design decisions: - - `mock()` / `powerMock()` → `Mockito.mock()` (inline mock maker handles finals). - - `mockStatic()` → `Mockito.mockStatic()`, returns `MockedStatic`, opened immediately during expect blocks. - - `mockConstructor()` / `constructor().build()` → creates pre-configured mock; defers `Mockito.mockConstruction()` to `run()` with delegation via `Method.invoke()`. - - `capture()` → `ArgumentCaptor.forClass()`. - - `partialMock()` → `Mockito.mock(type, CALLS_REAL_METHODS)`. - - `run()` lifecycle: execute expect blocks → open construction mocks → execute test blocks → close all scoped mocks. -- Added `mockito-core` (test scope) to `pom.xml`. -- **Validation:** Compiles, 334 existing tests still pass, `mvn install` succeeds. - -### 1.7.2 — Migrate Simple MockUnit Tests (unit.mock() only) ✅ - -- **DONE.** 44 files migrated from EasyMock to Mockito and moved from `java-excluded/` to `java/`. -- Mechanical migration (regex-based script) + manual fixes for 6 files. -- Key issues discovered and resolved: - - **Sequential return semantic gap:** EasyMock `expect().andReturn("a"); expect().andReturn("b")` is ordered; Mockito `when().thenReturn("a"); when().thenReturn("b")` overrides. Fix: `thenReturn("a", "b")`. Only 1 file (`OptionsHandlerTest`) had this within a single MockUnit block. - - **Void method arg capturing:** `unit.capture()` in void method context doesn't work with `ArgumentCaptor` (no `when()` wrapper). Fix: explicit `doAnswer()` with `AtomicReference` in SseTest. - - **Constructor arg capturing:** `unit.capture()` in `build()` context registers orphaned Mockito matchers. Fix: `ConstructorArgCapture` inner class + `ThreadSafeMockingProgress.pullLocalizedMatchers()`. - - **ByteBuddy corruption:** EasyMock + Mockito coexistence in same JVM corrupts generated `Method` objects (`NullPointerException` at `Method.getParameterTypes()`). Fix: `reuseForks=false` in surefire. -- 1 file (`LogbackConfTest`) deferred — classpath issue, not mock-related. -- **Validation:** 661 tests pass, 0 failures. - -### 1.7.3 — Migrate mockStatic Tests ✅ - -- **DONE.** 12 files migrated that use `unit.mockStatic()` but NOT `mockConstructor`. -- Static method stubbing converted: `when(X.method()).thenReturn(val)` → `unit.mockStatic(X.class).when(() -> X.method()).thenReturn(val)` -- `System.class` cannot be mocked by Mockito — 2 tests (CookieImplTest) rewritten with pattern assertions, 1 test (RequestLoggerTest) rewritten with regex assertion. -- Void method captures (3 files) converted to explicit `doAnswer()` with `AtomicReference`. -- `partialMock(FileChannel.class)` → `mock(FileChannel.class)` — CALLS_REAL_METHODS on FileChannel.close() causes NPE. -- **Validation:** 751 tests pass, 0 failures. - -### 1.7.4 — Migrate mockConstructor Tests ✅ - -- **DONE.** 5 files migrated that use `unit.mockConstructor()` / `unit.constructor()`. -- MockUnit enhanced: `preMockToConstructed` reverse map resolves pre-mock → construction mock in `get()`/`first()`. -- Void method captures (WebSocketImplTest, 7 tests) converted to `doAnswer()` + `AtomicReference`. -- Identity assertions (WsBinaryMessageTest, 2 tests) rewritten: `assertEquals(preMock, constructed)` → `assertNotNull` + `isMock()`. -- 4 files deferred: LogbackConfTest (classpath), RequestScopeTest (Guice internals), JettyServerTest + JettyHandlerTest (Jetty 10 API change). -- **Validation:** 807 tests pass, 0 failures. - -### 1.7.5 — Migrate Complex Tests (mockStatic + mockConstructor) ✅ - -- **DONE.** 5 files migrated that use BOTH `mockStatic` AND `mockConstructor`. -- 1 file (`FileConfTest`) deferred — same `NoClassDefFoundError: org/jooby/Jooby` as LogbackConfTest (Jooby static init requires PowerMock classloader). -- **Key issues discovered and resolved:** - - **MockUnit `setAccessible(true)`:** `openConstructionMocks()` delegates via `Method.invoke()` which fails on package-private inner classes (e.g., `SessionImpl$Builder`). Fix: add `method.setAccessible(true)` before delegation. - - **MockUnit `mockConstructor()` matcher cleanup:** Like `build()`, `mockConstructor()` must call `pullLocalizedMatchers()` and drain `pendingConstructorCaptures` to prevent orphaned matchers from `unit.capture()` args. - - **Pre-mock ≠ constructed mock identity:** `unit.get()` returns pre-mock during expect blocks; constructed mock is a different object at runtime. When pre-mock is used as argument to `when()` stubbing, the stub won't match. Fix: use `any()` matcher instead (ServerSessionManagerTest). - - **Route line number assertions:** RouteMetadataTest has inner class `Mvc` whose bytecode line numbers shift when imports/annotations change. All 6 line assertions updated (+10 offset). - - **Void method captures in JoobyTest (46 occurrences):** `binding.toInstance(unit.capture(Route.Definition.class))` is illegal in Mockito (matchers in void context). Fix: `addVoidCapture()` method in MockUnit + `doAnswer().when(binding).toInstance(any())` pattern. - - **Void method calls with matchers (~30 occurrences):** Lines like `binding.toInstance(isA(Env.class))` have orphaned matchers. Fix: remove the lines (void calls on mocks are no-ops in Mockito). - - **`Runtime.availableProcessors()` is native:** Cannot be mocked by Mockito's inline mock maker. Fix: removed the stubbing (production code uses real CPU count). - - **`MockedStatic.when()` leaks stubbing state:** A void mock call (e.g., `tc.configure(binder)`) immediately before `MockedStatic.when()` causes `CannotStubVoidMethodWithReturnValue`. Fix: removed unnecessary void mock calls that preceded MockedStatic operations. -- **Validation:** 894 tests pass, 0 failures. - -### 1.7.6 — Migrate Remaining Utilities ✅ - -- **DONE.** 4 non-MockUnit files moved from `java-excluded/` to `java/`. -- No EasyMock/PowerMock references — these are pure integration test infrastructure (JUnit runner, - HTTP client wrapper, base classes). -- Added Apache HttpClient 4.5.14 test dependencies: `httpclient`, `httpcore`, `fluent-hc`, `httpmime`. -- `SseFeature.java` deferred — hardwired to Ning AsyncHttpClient (`com.ning.http.client`) which is - not used anywhere in Kill Bill repositories. -- **Validation:** 894 tests pass (no new tests — these are utilities, not test classes), 0 failures. - -### 1.7.7 — Cleanup and Finalize ✅ - -- **DONE.** EasyMock fully removed from the jooby module. -- Removed `easymock` dependency from `jooby/pom.xml`. `mockito-core` (managed by parent) is the only - test mock framework. -- Migrated last 2 EasyMock holdouts: `ParamConverterTest` and `MutantImplTest` — both only used - `createMock()`, replaced with `Mockito.mock()`. -- `java-excluded/` has 6 remaining files, all blocked by non-mock issues (documented in 1.7.5/1.7.6). -- `-Pjooby` profile retained: `reuseForks=false` still needed for Mockito inline mock maker stability; - `java-excluded/` still has files that would fail compilation. -- **Validation:** `mvn clean install -pl jooby -Pjooby` — 894 tests pass, 0 failures. - Root build (`mvn clean install -DskipTests`) passes (pre-existing `jackson-annotations` unused - dependency warning is unrelated). - ---- - -## File Inventory - -| Category | Count | Status | -|---|---|---| -| MockUnit only (no static/constructor) | 44 | ✅ Migrated (Phase 1.7.2) | -| mockStatic only | 12 | ✅ Migrated (Phase 1.7.3) | -| mockConstructor only | 5 | ✅ Migrated (Phase 1.7.4) | -| mockStatic + mockConstructor | 5 | ✅ Migrated (Phase 1.7.5) | -| Non-MockUnit utilities / other | 4 | ✅ Migrated (Phase 1.7.6) | -| Deferred (not mock-related) | 6 | FileConfTest, LogbackConfTest, RequestScopeTest, JettyServerTest, JettyHandlerTest, SseFeature | -| Remaining in `java-excluded/` | 6 | Sum of above deferred | - -## Progress - -- [x] 1.7.1 — Rewrite MockUnit.java -- [x] 1.7.2 — Migrate 44 simple MockUnit tests -- [x] 1.7.3 — Migrate 12 mockStatic tests -- [x] 1.7.4 — Migrate 5 mockConstructor tests -- [x] 1.7.5 — Migrate 5 complex tests (static + constructor) -- [x] 1.7.6 — Migrate 4 remaining utilities -- [x] 1.7.7 — Cleanup and finalize diff --git a/jooby/CHANGES.md b/jooby/CHANGES.md index 87707246..c2a2622b 100644 --- a/jooby/CHANGES.md +++ b/jooby/CHANGES.md @@ -26,6 +26,8 @@ The following files were modified from upstream to adapt to Jetty 10 API changes | `JettyHandler.java` | Removed `WebSocketServerFactory` field/parameter; replaced `Request.MULTIPART_CONFIG_ELEMENT` with string constant; simplified `upgrade()` method | `WebSocketServerFactory` removed in Jetty 10; `MULTIPART_CONFIG_ELEMENT` constant removed from `Request` | | `JettyServer.java` | Removed `WebSocketPolicy`/`WebSocketServerFactory`/`DecoratedObjectFactory` imports and usage; changed `new SslContextFactory()` → `new SslContextFactory.Server()` | WebSocket API completely restructured in Jetty 10; `SslContextFactory` made abstract with `Server` subclass | | `Response.java` | `Response.Forwarding.setResetHeadersOnError()`: changed `this.setResetHeadersOnError(value)` → `rsp.setResetHeadersOnError(value)` | Upstream bug — infinite recursion. Every other method in `Forwarding` delegates to `rsp`; this one called `this` by mistake | +| `RoutePattern.java` | Simplified the glob-route regex to remove nested ambiguous quantifiers | Fixes CodeQL ReDoS warning without changing route-matching semantics | +| `PemReader.java` | Simplified PEM block regex whitespace handling from redundant alternation to `\\s+` | Fixes CodeQL ReDoS warning while keeping the same accepted PEM formats | ## POM / Dependency Changes @@ -51,19 +53,19 @@ Differences from upstream dependency versions: | `org.slf4j:slf4j-api` | 1.7.x | 2.0.9 (managed) | Kill Bill standardized version | | `org.powermock:powermock-*` | 2.0.0 | **removed** | Not managed by killbill-oss-parent; obsolete for modern JDKs | | `jakarta.annotation:jakarta.annotation-api` | not present | 1.3.5 (managed) | Added for `@PostConstruct`/`@PreDestroy` in `LifeCycle.java` | -| `com.github.spotbugs:spotbugs-annotations` | not present | **not included** | Will be added in Phase 1.8 (SpotBugs triage) | +| `com.github.spotbugs:spotbugs-annotations` | not present | **not included** | Not needed; no forked source uses `@SuppressFBWarnings`, and SpotBugs triage uses the exclusion filter instead | | `org.eclipse.jetty:jetty-alpn-server` | not present | 10.0.16 | Required by `JettyServer.java` for ALPN/HTTP2 support | | `org.eclipse.jetty.websocket:websocket-jetty-api` | not present (was part of websocket-server) | 10.0.16 | Jetty 10 split WebSocket API into separate artifact | | `org.eclipse.jetty:jetty-io` | transitive | 10.0.16 (explicit) | Used directly in source; declared explicitly to satisfy dependency:analyze | | `org.eclipse.jetty:jetty-util` | transitive | 10.0.16 (explicit) | Used directly in source; declared explicitly to satisfy dependency:analyze | | `javax.inject:javax.inject` | transitive via Guice | managed (explicit) | Used directly in source; declared explicitly to satisfy dependency:analyze | | `junit:junit` | optional (compile) | compile + optional | Parent forces test scope; explicit compile needed for `JoobyRule` | -| `org.mockito:mockito-core` | not present | 5.3.1 (managed, test) | Added for Phase 1.7 EasyMock→Mockito migration | -| `org.easymock:easymock` | present (test) | **removed** | Replaced by mockito-core in Phase 1.7.7 | -| `org.apache.httpcomponents:httpclient` | not present | 4.5.14 (test) | Integration test HTTP client (Phase 1.7.6) | -| `org.apache.httpcomponents:httpcore` | not present | 4.4.16 (test) | Required by httpclient (Phase 1.7.6) | -| `org.apache.httpcomponents:fluent-hc` | not present | 4.5.14 (test) | Client.java fluent Executor API (Phase 1.7.6) | -| `org.apache.httpcomponents:httpmime` | not present | 4.5.14 (test) | Client.java multipart support (Phase 1.7.6) | +| `org.mockito:mockito-core` | not present | 5.3.1 (managed, test) | Sole active mocking framework for the migrated test tree | +| `org.easymock:easymock` | present (test) | **removed** | Replaced by mockito-core in the active test tree | +| `org.apache.httpcomponents:httpclient` | not present | 4.5.14 (test) | Integration test HTTP client | +| `org.apache.httpcomponents:httpcore` | not present | 4.4.16 (test) | Required by httpclient | +| `org.apache.httpcomponents:fluent-hc` | not present | 4.5.14 (test) | `Client.java` fluent Executor API | +| `org.apache.httpcomponents:httpmime` | not present | 4.5.14 (test) | `Client.java` multipart support | ## Structural Changes @@ -72,9 +74,9 @@ Differences from upstream dependency versions: | 4 upstream modules + funzy merged into 1 flat module | Kill Bill convention (like `killbill-jdbi`, `killbill-config-magic`) | | `jooby-netty` excluded | Kill Bill uses Jetty; SSE/WebSocket work via core SPI | | ASM shade plugin preserved | Relocates `org.objectweb.asm` → `org.jooby.internal.asm` (same as upstream) | -| Test compilation disabled by default | 76 of 125 test files depend on PowerMock (not available); enabled via `-Pjooby` profile | -| 20 test files moved to `src/test/java-excluded/` | Were blocked by PowerMock/missing deps; 14 restored in Phases 1.7.2-1.7.6, 6 remain (non-mock blockers) | -| 105 test files remain in `src/test/java/` | 50 pre-existing + 43 migrated (1.7.2) + 12 migrated (1.7.3); compile and run with `-Pjooby` profile (751 tests pass) | +| Jooby tests now run in the default Maven lifecycle | The earlier PowerMock-era gating was removed after all deferred tests were restored into the active test tree | +| 20 test files moved to `src/test/java-excluded/` | Were blocked by PowerMock/missing deps; all 20 have now been restored into the active test tree | +| 124 Java files remain in `src/test/java/` | Active test tree after migration, including shared test utilities; the standard Maven test lifecycle runs 108 test classes / 923 tests successfully | | SpotBugs exclude filter (`spotbugs-exclude.xml`) | Targeted exclusions for 77 upstream findings (12 bug patterns across 10 categories) triaged as intentional framework patterns or low-risk upstream code | | Apache RAT exclusions for resources | Resource files (`.conf`, `.xml`, `.properties`, SSL certs) have no license headers | @@ -83,189 +85,64 @@ Differences from upstream dependency versions: None. All resource files (`web.xml`, `jooby.conf`, `server.conf`, SSL certs, `mime.properties`, test configs) are byte-identical to upstream. -## Test Framework Migration (Phase 1.7) +## Test Infrastructure Changes -Upstream tests use EasyMock + PowerMock. These are being migrated to **Mockito 5** (`mockito-core:5.3.1`). +Upstream tests used EasyMock + PowerMock. The active Kill Bill fork now uses **Mockito 5** +(`mockito-core:5.3.1`) as its sole mocking framework. -### Sub-phase 1.7.1 — MockUnit.java Rewrite ✅ +### MockUnit Rewrite -`src/test/java/org/jooby/test/MockUnit.java` completely rewritten (not a modification of upstream). -The upstream version used EasyMock record-replay + PowerMock static/constructor mocking. -The new version uses pure Mockito 5 APIs: +`src/test/java/org/jooby/test/MockUnit.java` was rewritten around Mockito 5 APIs instead of the +upstream EasyMock record/replay + PowerMock static/constructor mocking model. | Old API (EasyMock/PowerMock) | New API (Mockito 5) | |---|---| | `EasyMock.createMock()` | `Mockito.mock()` | -| `PowerMock.createMock()` (finals) | `Mockito.mock()` (inline mock maker handles finals natively) | +| `PowerMock.createMock()` (finals) | `Mockito.mock()` | | `PowerMock.mockStatic()` + `EasyMock.expect(Static.method())` | `Mockito.mockStatic()` returning `MockedStatic` | | `PowerMock.createMockAndExpectNew()` / `MockUnit.constructor().build()` | Pre-mock + deferred `Mockito.mockConstruction()` with delegation | | `EasyMock.capture()` / `captured()` | `ArgumentCaptor.forClass().capture()` / `getValue()` | -| `PowerMock.replay()` / `PowerMock.verify()` | Not needed — Mockito stubs are active immediately | +| `PowerMock.replay()` / `PowerMock.verify()` | Not needed; Mockito stubs are active immediately | | `partialMock(type, methods)` | `Mockito.mock(type, CALLS_REAL_METHODS)` | -Key design: Constructor mocking uses a "pre-mock + delegation" pattern. `build()` creates a Mockito mock -that callers configure with `when()`. At `run()` time, `MockedConstruction` is opened; each constructed mock -delegates all calls to its corresponding pre-mock via `Method.invoke()`. +Notable implementation changes in `MockUnit.java`: +- constructor mocking uses a pre-mock + delegation pattern; constructed mocks delegate back to the + corresponding pre-mock via reflection +- `ConstructorArgCapture` and pending capture queues preserve constructor argument capture support +- `captured()` now merges values from argument captors, constructor captures, and explicit void captures +- `openConstructionMocks()` uses `setAccessible(true)` for package-private inner-class delegation +- `preMockToConstructed` resolves pre-mock to constructed mock identity when tests compare both forms -### Sub-phase 1.7.2 — Simple MockUnit Test Migration ✅ +### Migrated and Rewritten Tests -44 test files migrated from EasyMock to Mockito syntax (moved from `java-excluded/` to `src/test/java/`). +All 20 files that had been moved to `src/test/java-excluded/` during the migration are now restored +to the active test tree. `src/test/java-excluded/` is empty. -**Mechanical changes applied to all 44 files:** -- `EasyMock.expect(x).andReturn(y)` → `Mockito.when(x).thenReturn(y)` -- `EasyMock.expectLastCall()` → removed (void stubs not needed in Mockito) -- `expect().andThrow()` → `when().thenThrow()` / `doThrow().when()` -- `@RunWith(PowerMockRunner.class)` / `@PrepareForTest` annotations removed -- Import replacements: `org.easymock.*` → `org.mockito.*` - -**Manual fixes for specific files:** - -| File | Change | Reason | -|---|---|---| -| `Issue1087.java` | Removed `EasyMock.aryEq()` wrapper | Void method doesn't need argument matcher | -| `RouteDefinitionTest.java` | Line number assertion `9→24` | Kill Bill license header adds 15 lines | -| `RequestTest.java` | Merged sequential `when().thenReturn()` | Mockito overrides; use `thenReturn(a, b)` for ordered returns | -| `JacksonParserTest.java` | Cast `null` to `(java.lang.reflect.Type)` | Overload disambiguation for `parse(Type)` vs `parse(MediaType)` | -| `OptionsHandlerTest.java` | Created `routeMethods(String...)` varargs helper | Only file with true sequential return pattern within same MockUnit block | -| `SseTest.java` | Rewrote 3 methods with explicit `doAnswer()` captors | Void method arg capturing requires `doAnswer()` instead of `ArgumentCaptor` | - -**Files excluded from migration (non-mock issues):** - -| File | Reason | Status | -|---|---|---| -| `LogbackConfTest.java` | `NoClassDefFoundError: org/jooby/Jooby` (static init classpath issue) | Remains in `java-excluded/` | - -**Surefire configuration changes:** - -| Setting | Value | Reason | -|---|---|---| -| `reuseForks` | `false` | EasyMock + Mockito coexistence corrupts ByteBuddy-generated `Method` objects when sharing JVM across test classes | -| `argLine` | `-XX:-OmitStackTraceInFastThrow --illegal-access=permit` | Full stack traces for debugging; JDK 11 module access | - -**MockUnit.java changes for Phase 1.7.2:** -- `ConstructorArgCapture` inner class + pending capture queue for `build()` context -- `build()` clears orphaned Mockito matchers via `ThreadSafeMockingProgress.pullLocalizedMatchers()` -- `captured()` merges from ArgumentCaptors + constructor arg captures -- `openConstructionMocks()` populates constructor captures from `context.arguments()` - -**Result:** 661 tests pass (327 pre-existing + 334 migrated), 0 failures. - -### Sub-phase 1.7.3 — mockStatic Test Migration ✅ - -12 test files migrated that use `unit.mockStatic()` for static method stubbing. - -**Static mock conversion pattern:** -- `unit.mockStatic(X.class); when(X.method(args)).thenReturn(val)` → `unit.mockStatic(X.class).when(() -> X.method(args)).thenReturn(val)` -- No-arg static methods use method reference: `unit.mockStatic(X.class).when(X::method).thenReturn(val)` - -**Additional fixes:** - -| File | Change | Reason | -|---|---|---| -| `CookieImplTest.java` | Rewrote 2 tests to not mock `System.class` | Mockito cannot mock `java.lang.System` (class loader interference) | -| `RequestLoggerTest.java` | Rewrote `latency` test with regex assertion; void capture → `doAnswer()` | Cannot mock `System.class`; `rsp.complete()` is void | -| `DefaultErrHandlerTest.java` | Void capture → `doAnswer()` with `AtomicReference` | `rsp.send(unit.capture(...))` is void method | -| `JettyResponseTest.java` | Void capture → `doAnswer()` with `AtomicReference` | `output.sendContent(unit.capture(...))` is void method | -| `ServletServletResponseTest.java` | `partialMock(FileChannel.class)` → `mock(FileChannel.class)` | `CALLS_REAL_METHODS` on `FileChannel.close()` causes NPE | -| `CookieSignatureTest.java` | Removed `@PowerMockIgnore` annotation | Not needed in Mockito | - -**Result:** 751 tests pass (661 prior + 90 new), 0 failures. - -### Sub-phase 1.7.4 — mockConstructor Test Migration ✅ - -5 test files migrated that use `unit.mockConstructor()`/`unit.constructor()` for constructor mocking, -plus 1 file (`RequestScopeTest`) identified as already-Mockito but blocked by Guice internal API. - -**MockUnit enhancement:** -- Added `preMockToConstructed` reverse map: resolves pre-mock → construction mock in `get()`/`first()`, - fixing identity mismatches when tests compare `unit.get()` results with objects from `new`. - -**Additional fixes:** +Notable rewrites and follow-up restorations: | File | Change | Reason | |---|---|---| -| `WebSocketImplTest.java` | 7 void method captures → `doAnswer()` + `AtomicReference`; `expectLastCall().andThrow()` → `doThrow()` | Void methods (`onTextMessage`, `onErrorMessage`, `onCloseMessage`) can't use `ArgumentCaptor` | -| `WsBinaryMessageTest.java` | 2 tests rewritten: `assertEquals(preMock, constructed)` → `assertNotNull` + `isMock()` | MockedConstruction returns different object than pre-mock; identity comparison fails | - -**Deferred files:** - -| File | Reason | -|---|---| -| `LogbackConfTest.java` | `NoClassDefFoundError: org/jooby/Jooby` (static init classpath issue) | -| `RequestScopeTest.java` | `CircularDependencyProxy` (Guice internal API, not accessible in Java 11 module system) | -| `JettyServerTest.java` | Uses `WebSocketServerFactory` (removed in Jetty 10) | -| `JettyHandlerTest.java` | Uses `WebSocketServerFactory` (removed in Jetty 10) | - -**Result:** 807 tests pass (751 prior + 56 new), 0 failures. - -### Sub-phase 1.7.5 — Complex Test Migration (mockStatic + mockConstructor) ✅ - -5 test files migrated that use BOTH `mockStatic` AND `mockConstructor`. 1 file (`FileConfTest`) -deferred — same `NoClassDefFoundError` as LogbackConfTest (Jooby static init requires PowerMock classloader). - -**MockUnit enhancements:** -- Added `method.setAccessible(true)` in `openConstructionMocks()` delegation — package-private inner - classes (e.g., `SessionImpl$Builder`) require accessible flag for `Method.invoke()`. -- Added matcher cleanup (`pullLocalizedMatchers()`) and capture drain to `mockConstructor()` method - — matches existing `build()` behavior to prevent orphaned matchers from `unit.capture()` args. -- Added `addVoidCapture(type, value)` method and `voidCaptures` map — enables `doAnswer()` based - capturing for void methods. `captured()` merges values from ArgumentCaptors, constructor captures, - AND void captures. - -**Migrated files:** - -| File | Tests | Key Changes | -|---|---|---| -| `RouteMetadataTest.java` | 10 | Line number assertions updated (+10 offset) | -| `BodyReferenceImplTest.java` | 11 | Straightforward mockStatic + mockConstructor migration | -| `CookieSessionManagerTest.java` | 9 | `doAnswer()` + `AtomicReference` for void captures | -| `ServerSessionManagerTest.java` | 13 | `any(Session.Builder.class)` for pre-mock identity mismatch | -| `JoobyTest.java` | 44 | Largest migration (3000 lines); see additional details below | - -**JoobyTest-specific fixes:** -- 46 `binding.toInstance(unit.capture(Route.Definition.class))` calls → single `doAnswer()` per expect - block using `unit.addVoidCapture()`. -- ~30 void mock calls with matchers (`toInstance(isA(...))`, `install(any(...))`, etc.) → removed - entirely (void calls on mocks are no-ops in Mockito). -- `Runtime.availableProcessors()` is native — cannot be mocked by Mockito inline mock maker. - Removed the stubbing; production code uses real CPU count. -- `MockedStatic.when()` leaks stubbing state — void mock calls immediately preceding `MockedStatic` - operations (e.g., `tc.configure(binder)`) cause `CannotStubVoidMethodWithReturnValue`. Removed - these unnecessary void calls. -- `module.configure(isA(...), isA(...), eq(...))` → `module.configure(null, null, binder)` (matchers - in void context). - -**Deferred file:** - -| File | Reason | -|---|---| -| `FileConfTest.java` | `NoClassDefFoundError: org/jooby/Jooby` (same as LogbackConfTest) | - -**Result:** 894 tests pass (807 prior + 87 new), 0 failures. - -### Sub-phase 1.7.6 -- Integration Test Utilities - -4 non-MockUnit utility files moved from java-excluded/ to java/. These are the integration test -infrastructure (JUnit runner, HTTP client wrapper, base classes) -- no EasyMock/PowerMock references. - -New test-scope dependencies in pom.xml: -- org.apache.httpcomponents:httpclient 4.5.14 (Client.java HTTP test client) -- org.apache.httpcomponents:httpcore 4.4.16 (required by httpclient, flagged by dependency analysis) -- org.apache.httpcomponents:fluent-hc 4.5.14 (Client.java fluent Executor API) -- org.apache.httpcomponents:httpmime 4.5.14 (Client.java multipart upload support) - -Deferred: SseFeature.java -- hardwired to Ning AsyncHttpClient (com.ning.http.client), not used in Kill Bill. - -Result: 894 tests pass (no new tests -- these are utilities), 0 failures. 6 files remain in java-excluded/. - -### Sub-phase 1.7.7 -- Cleanup and Finalize - -Removed easymock dependency from pom.xml. Migrated last 2 EasyMock holdouts -(ParamConverterTest and MutantImplTest) which only used createMock() -- replaced -with Mockito.mock(). No EasyMock or PowerMock references remain in active test code. -mockito-core (managed by killbill-oss-parent) is now the sole test mock framework. - -The -Pjooby profile is retained: reuseForks=false is still needed for Mockito inline -mock maker stability, and java-excluded/ still has 6 files that would fail compilation. - -Result: 894 tests pass, 0 failures. EasyMock migration complete. +| `CookieImplTest.java` | Reworked assertions to avoid mocking `System.class` | Mockito cannot mock `java.lang.System` reliably | +| `RequestLoggerTest.java` | Reworked latency assertion and void captures | Avoids `System` mocking and adapts to Mockito void stubbing | +| `DefaultErrHandlerTest.java` | Void captures rewritten with `doAnswer()` | `rsp.send(...)` is a void method | +| `JettyResponseTest.java` | Void captures rewritten with `doAnswer()` | `output.sendContent(...)` is a void method | +| `ServletServletResponseTest.java` | `partialMock(FileChannel.class)` replaced with `mock(FileChannel.class)` | `CALLS_REAL_METHODS` caused close-path failures | +| `FileConfTest.java` | Rewritten as a real filesystem test | Replaces EasyMock + PowerMock constructor/static mocking | +| `LogbackConfTest.java` | Rewritten as a real filesystem/config-driven test | Replaces MockUnit-based lookup stubbing | +| `RequestScopeTest.java` | Rewritten as a direct behavior test | Exercises circular-proxy handling without a compile-time Guice internal type dependency | +| `JettyHandlerTest.java` | Rewritten around current Jetty 10 adapter behavior | Upstream websocket-era expectations no longer matched the fork | +| `JettyServerTest.java` | Rewritten around real `Server`, `ServerConnector`, and `ContextHandler` objects | Replaces removed Jetty 9 websocket factory assumptions | +| `SseFeature.java` | Rewritten to use JDK 11 `HttpClient` | Replaces removed Ning AsyncHttpClient dependency | + +### Current Test Baseline + +- Jooby tests run in the default Maven lifecycle +- `reuseForks=false` remains configured in Surefire for stable Mockito inline runs +- active test tree: `124` Java files in `src/test/java` +- runnable suite: `108` test classes / `923` tests + +### Additional Test-Tree Cleanup + +- `ParamConverterTest` and `MutantImplTest` were the last direct EasyMock holdouts; both now use `Mockito.mock()` +- `Issue1087.java` was deleted because it was the only direct `@JsonView` / `jackson-annotations` consumer in the forked test tree +- the direct `jackson-annotations` dependency path was removed from `pom.xml` after `Issue1087.java` was deleted diff --git a/jooby/README.md b/jooby/README.md index d698a536..b65b25d0 100644 --- a/jooby/README.md +++ b/jooby/README.md @@ -18,21 +18,19 @@ Not forked: ## Building & Testing -Default build (compile main sources only, skip tests): +`killbill-jooby` keeps the upstream **JUnit 4** test stack. It does **not** use the +repository's usual TestNG-based test setup. + +Standard build: ``` -mvn clean install -pl jooby +mvn clean install ``` -Run tests (103 test files, 894 tests): +Run tests (108 test classes, 923 tests): ``` -mvn clean test -pl jooby -Pjooby +mvn clean test ``` -**Note:** 6 test files remain in `src/test/java-excluded/`: FileConfTest and LogbackConfTest (Jooby -static init needs PowerMock classloader), RequestScopeTest (Guice internal API), JettyServerTest -and JettyHandlerTest (Jetty 10 API removal), and SseFeature (Ning AsyncHttpClient dependency). -The `-Pjooby` profile compiles and runs only the test files in `src/test/java/`. - Changes with upstream: ``` diff --git a/jooby/phase-2-plans.md b/jooby/phase-2-plans.md new file mode 100644 index 00000000..896dd04e --- /dev/null +++ b/jooby/phase-2-plans.md @@ -0,0 +1,43 @@ +# Phase 2 Plans + +This document records the current Jooby fork baseline that Phase 2 work should preserve while +upgrading the repository foundation. + +## Current Baseline + +- Root build baseline validated on Temurin **JDK 11.0.26** with **Maven 3.8.5** +- Root build command used for baseline: + - `mvn -q clean install -DskipTests` +- Jooby module test baseline: + - `mvn clean test` +- Active Jooby test tree: + - `124` Java files in `src/test/java` + - `108` runnable test classes + - `923` tests + - `src/test/java-excluded/` is empty + +## Current Dependency Baseline Relevant to Phase 2 + +| Area | Current state | +|---|---| +| JDK target | Repository baseline is still JDK 11 | +| Guice | `com.google.inject:guice` is `5.1.0` via `killbill-oss-parent` | +| Guice servlet bridge | `com.google.inject.extensions:guice-servlet` is still on the current parent-managed line | +| Inject namespace | Source still uses `javax.inject`; `javax.inject:javax.inject` is explicitly declared in the Jooby fork | +| Servlet namespace | `jakarta.servlet:jakarta.servlet-api:4.0.4` is used as a transitional artifact and still exposes `javax.servlet` packages | +| Jetty | Jetty already sits on `10.0.16` in the fork | + +## Phase 2 Sequence + +1. Upgrade the build baseline to JDK 17 without changing `javax.*` source imports +2. Re-establish a green build/test baseline on JDK 17 +3. Upgrade Guice and guice-servlet to 6.0.0 +4. Re-establish a green build/test baseline before starting any Jakarta namespace migration + +## Notes + +- `jooby/CHANGES.md` now focuses on durable source, dependency, and test-tree changes rather than + phased execution history. +- The Jooby module is a useful canary for Phase 2 because it already exercises Guice, servlet, Jetty, + and a large migrated Mockito-based test tree. +- The old `-Pjooby` test gate was removed after the active test tree was fully restored. diff --git a/jooby/pom.xml b/jooby/pom.xml index 63e888ca..a78aa0c1 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -30,27 +30,23 @@ spotbugs-exclude.xml - + com.google.inject guice - com.google.guava guava - com.typesafe config - org.slf4j slf4j-api - com.google.code.findbugs jsr305 @@ -72,6 +68,7 @@ jakarta.servlet jakarta.servlet-api + org.eclipse.jetty @@ -111,7 +108,8 @@ javax.inject javax.inject - + + com.fasterxml.jackson.core jackson-databind @@ -136,12 +134,15 @@ jackson-module-afterburner ${jackson.version} + + junit junit compile true + ch.qos.logback @@ -207,21 +208,21 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - - default-testCompile - none - - - + org.apache.maven.plugins maven-surefire-plugin + 3.0.0-M7 + + + org.apache.maven.surefire + surefire-junit47 + 3.0.0-M7 + + - true + false + false @@ -232,50 +233,9 @@ src/main/resources/** src/test/resources/** - src/test/java-excluded/** - - - jooby - - - - org.apache.maven.plugins - maven-compiler-plugin - - - default-testCompile - test-compile - - testCompile - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M7 - - false - false - -XX:-OmitStackTraceInFastThrow --illegal-access=permit - - - - org.apache.maven.surefire - surefire-junit47 - 3.0.0-M7 - - - - - - - diff --git a/jooby/src/main/java/org/jooby/Cookie.java b/jooby/src/main/java/org/jooby/Cookie.java index 2cf5cbd4..d29179d2 100644 --- a/jooby/src/main/java/org/jooby/Cookie.java +++ b/jooby/src/main/java/org/jooby/Cookie.java @@ -446,8 +446,8 @@ public static String sign(final String value, final String secret) { try { Mac mac = Mac.getInstance(HMAC_SHA256); - mac.init(new SecretKeySpec(secret.getBytes(), HMAC_SHA256)); - byte[] bytes = mac.doFinal(value.getBytes()); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256)); + byte[] bytes = mac.doFinal(value.getBytes(StandardCharsets.UTF_8)); return EQ.matcher(BaseEncoding.base64().encode(bytes)).replaceAll("") + SEP + value; } catch (Exception ex) { throw new IllegalArgumentException("Can't sign value", ex); diff --git a/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java b/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java index 3b8b23e7..3635a470 100644 --- a/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java +++ b/jooby/src/main/java/org/jooby/handlers/CsrfHandler.java @@ -153,7 +153,7 @@ public void handle(final Request req, final Response rsp, final Route.Chain chai String candidate = req.header(name).toOptional() .orElseGet(() -> req.param(name).toOptional().orElse(null)); if (!token.equals(candidate)) { - throw new Err(Status.FORBIDDEN, "Invalid Csrf token: " + candidate); + throw new Err(Status.FORBIDDEN, "Invalid CSRF token"); } } diff --git a/jooby/src/main/java/org/jooby/handlers/SSIHandler.java b/jooby/src/main/java/org/jooby/handlers/SSIHandler.java index f4f6e60b..d5d81ca2 100644 --- a/jooby/src/main/java/org/jooby/handlers/SSIHandler.java +++ b/jooby/src/main/java/org/jooby/handlers/SSIHandler.java @@ -149,14 +149,18 @@ private String process(final Env env, final String src) { private String file(final String key) { String file = Route.normalize(key.trim()); - return text(getClass().getResourceAsStream(file)); + InputStream stream = getClass().getResourceAsStream(file); + if (stream == null) { + throw new NoSuchElementException("Resource not found: " + file); + } + return text(stream); } private String text(final InputStream stream) { try (InputStream in = stream) { - return CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8)); - } catch (IOException | NullPointerException x) { - throw new NoSuchElementException(); + return CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8)); + } catch (IOException x) { + throw new NoSuchElementException(x.getMessage()); } } } diff --git a/jooby/src/test/java-excluded/SseFeature.java b/jooby/src/test/java-excluded/SseFeature.java deleted file mode 100644 index b5a5ae36..00000000 --- a/jooby/src/test/java-excluded/SseFeature.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2026 The Billing Project, LLC - * - * The Billing Project licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jooby.test; - -import static org.junit.Assert.assertEquals; - -import java.nio.charset.StandardCharsets; -import java.util.concurrent.CountDownLatch; - -import org.jooby.Jooby; -import org.jooby.MediaType; -import org.junit.After; -import org.junit.Before; -import org.junit.runner.RunWith; - -import com.ning.http.client.AsyncHandler; -import com.ning.http.client.AsyncHttpClient; -import com.ning.http.client.AsyncHttpClientConfig; -import com.ning.http.client.FluentCaseInsensitiveStringsMap; -import com.ning.http.client.HttpResponseBodyPart; -import com.ning.http.client.HttpResponseHeaders; -import com.ning.http.client.HttpResponseStatus; - -/** - * Internal use only. - * - * @author edgar - */ -@RunWith(JoobySuite.class) -public abstract class SseFeature extends Jooby { - - private int port; - - private AsyncHttpClient client; - - @Before - public void before() { - client = new AsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); - } - - @After - public void after() { - client.close(); - } - - public String sse(final String path, final int count) throws Exception { - CountDownLatch latch = new CountDownLatch(count); - String result = client.prepareGet("http://localhost:" + port + path) - .addHeader("Content-Type", MediaType.sse.name()) - .addHeader("last-event-id", count + "") - .execute(new AsyncHandler() { - - StringBuilder sb = new StringBuilder(); - - @Override - public void onThrowable(final Throwable t) { - t.printStackTrace(); - } - - @Override - public AsyncHandler.STATE onBodyPartReceived(final HttpResponseBodyPart bodyPart) - throws Exception { - sb.append(new String(bodyPart.getBodyPartBytes(), StandardCharsets.UTF_8)); - latch.countDown(); - return AsyncHandler.STATE.CONTINUE; - } - - @Override - public AsyncHandler.STATE onStatusReceived(final HttpResponseStatus responseStatus) - throws Exception { - assertEquals(200, responseStatus.getStatusCode()); - return AsyncHandler.STATE.CONTINUE; - } - - @Override - public AsyncHandler.STATE onHeadersReceived(final HttpResponseHeaders headers) - throws Exception { - FluentCaseInsensitiveStringsMap h = headers.getHeaders(); - assertEquals("close", h.get("Connection").get(0).toLowerCase()); - assertEquals("text/event-stream; charset=utf-8", - h.get("Content-Type").get(0).toLowerCase()); - return AsyncHandler.STATE.CONTINUE; - } - - @Override - public String onCompleted() throws Exception { - return sb.toString(); - } - }).get(); - - latch.await(); - return result; - } - -} diff --git a/jooby/src/test/java-excluded/org/jooby/FileConfTest.java b/jooby/src/test/java-excluded/org/jooby/FileConfTest.java deleted file mode 100644 index 2782c9e2..00000000 --- a/jooby/src/test/java-excluded/org/jooby/FileConfTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2026 The Billing Project, LLC - * - * The Billing Project licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jooby; - -import static org.easymock.EasyMock.expect; -import static org.junit.Assert.assertEquals; - -import java.io.File; - -import org.jooby.test.MockUnit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({Jooby.class, File.class, ConfigFactory.class }) -public class FileConfTest { - - @Test - public void rootFile() throws Exception { - Config conf = ConfigFactory.empty(); - new MockUnit() - .expect(unit -> { - unit.mockStatic(ConfigFactory.class); - }) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File root = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "app.conf"); - expect(root.exists()).andReturn(true); - - expect(ConfigFactory.parseFile(root)).andReturn(conf); - }) - .run(unit -> { - assertEquals(conf, Jooby.fileConfig("app.conf")); - }); - } - - @Test - public void confFile() throws Exception { - Config conf = ConfigFactory.empty(); - new MockUnit() - .expect(unit -> { - unit.mockStatic(ConfigFactory.class); - }) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File root = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "app.conf"); - expect(root.exists()).andReturn(false); - - File cdir = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "conf"); - - File cfile = unit.constructor(File.class) - .args(File.class, String.class) - .build(cdir, "app.conf"); - expect(cfile.exists()).andReturn(true); - - expect(ConfigFactory.parseFile(cfile)).andReturn(conf); - }) - .run(unit -> { - assertEquals(conf, Jooby.fileConfig("app.conf")); - }); - } - - @Test - public void empty() throws Exception { - Config conf = ConfigFactory.empty(); - new MockUnit() - .expect(unit -> { - unit.mockStatic(ConfigFactory.class); - }) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File root = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "app.conf"); - expect(root.exists()).andReturn(false); - - File cdir = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "conf"); - - File cfile = unit.constructor(File.class) - .args(File.class, String.class) - .build(cdir, "app.conf"); - expect(cfile.exists()).andReturn(false); - - expect(ConfigFactory.empty()).andReturn(conf); - }) - .run(unit -> { - assertEquals(conf, Jooby.fileConfig("app.conf")); - }); - } - -} diff --git a/jooby/src/test/java-excluded/org/jooby/LogbackConfTest.java b/jooby/src/test/java-excluded/org/jooby/LogbackConfTest.java deleted file mode 100644 index 49c399e5..00000000 --- a/jooby/src/test/java-excluded/org/jooby/LogbackConfTest.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2026 The Billing Project, LLC - * - * The Billing Project licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jooby; - -import static org.mockito.Mockito.when; -import static org.junit.Assert.assertEquals; - -import java.io.File; - -import org.jooby.test.MockUnit; -import org.jooby.test.MockUnit.Block; -import org.junit.Test; - -import com.typesafe.config.Config; - -public class LogbackConfTest { - - @Test - public void withConfigFile() throws Exception { - new MockUnit(Config.class) - .expect(conflog(true)) - .expect(unit -> { - Config config = unit.get(Config.class); - when(config.getString("logback.configurationFile")).thenReturn("logback.xml"); - }) - .run(unit -> { - assertEquals("logback.xml", Jooby.logback(unit.get(Config.class))); - }); - } - - @Test - public void rootFile() throws Exception { - new MockUnit(Config.class) - .expect(conflog(false)) - .expect(env(null)) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File conf = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "conf"); - - File rlogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "logback.xml"); - when(rlogback.exists()).thenReturn(false); - - File clogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(conf, "logback.xml"); - when(clogback.exists()).thenReturn(false); - }) - .run(unit -> { - assertEquals("logback.xml", Jooby.logback(unit.get(Config.class))); - }); - } - - @Test - public void rootFileFound() throws Exception { - new MockUnit(Config.class) - .expect(conflog(false)) - .expect(env(null)) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File conf = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "conf"); - - File rlogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "logback.xml"); - when(rlogback.exists()).thenReturn(true); - when(rlogback.getAbsolutePath()).thenReturn("foo/logback.xml"); - - unit.constructor(File.class) - .args(File.class, String.class) - .build(conf, "logback.xml"); - }) - .run(unit -> { - assertEquals("foo/logback.xml", Jooby.logback(unit.get(Config.class))); - }); - } - - @Test - public void confFile() throws Exception { - new MockUnit(Config.class) - .expect(conflog(false)) - .expect(env("foo")) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File conf = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "conf"); - - File relogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "logback.foo.xml"); - when(relogback.exists()).thenReturn(false); - - File rlogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "logback.xml"); - when(rlogback.exists()).thenReturn(false); - - File clogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(conf, "logback.xml"); - when(clogback.exists()).thenReturn(false); - - File celogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(conf, "logback.foo.xml"); - when(celogback.exists()).thenReturn(false); - }) - .run(unit -> { - assertEquals("logback.xml", Jooby.logback(unit.get(Config.class))); - }); - } - - @Test - public void confFileFound() throws Exception { - new MockUnit(Config.class) - .expect(conflog(false)) - .expect(env("foo")) - .expect(unit -> { - File dir = unit.constructor(File.class) - .args(String.class) - .build(System.getProperty("user.dir")); - - File conf = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "conf"); - - File relogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "logback.foo.xml"); - when(relogback.exists()).thenReturn(false); - - unit.constructor(File.class) - .args(File.class, String.class) - .build(dir, "logback.xml"); - - File celogback = unit.constructor(File.class) - .args(File.class, String.class) - .build(conf, "logback.foo.xml"); - when(celogback.exists()).thenReturn(true); - when(celogback.getAbsolutePath()).thenReturn("logback.foo.xml"); - - unit.constructor(File.class) - .args(File.class, String.class) - .build(conf, "logback.xml"); - }) - .run(unit -> { - assertEquals("logback.foo.xml", Jooby.logback(unit.get(Config.class))); - }); - } - - private Block env(final String env) { - return unit -> { - Config config = unit.get(Config.class); - when(config.hasPath("application.env")).thenReturn(env != null); - if (env != null) { - when(config.getString("application.env")).thenReturn(env); - } - }; - } - - private Block conflog(final boolean b) { - return unit -> { - Config config = unit.get(Config.class); - when(config.hasPath("logback.configurationFile")).thenReturn(b); - }; - } - -} diff --git a/jooby/src/test/java-excluded/org/jooby/internal/RequestScopeTest.java b/jooby/src/test/java-excluded/org/jooby/internal/RequestScopeTest.java deleted file mode 100644 index 33210a60..00000000 --- a/jooby/src/test/java-excluded/org/jooby/internal/RequestScopeTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2026 The Billing Project, LLC - * - * The Billing Project licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jooby.internal; - -import static org.mockito.Mockito.when; -import static org.junit.Assert.assertEquals; - -import java.util.Collections; -import java.util.Map; - -import org.jooby.test.MockUnit; -import org.junit.Test; - -import com.google.inject.Key; -import com.google.inject.OutOfScopeException; -import com.google.inject.Provider; -import com.google.inject.internal.CircularDependencyProxy; - -public class RequestScopeTest { - - @Test - public void enter() { - RequestScope requestScope = new RequestScope(); - requestScope.enter(Collections.emptyMap()); - requestScope.exit(); - } - - @SuppressWarnings({"unchecked", "rawtypes" }) - @Test - public void scopedValue() throws Exception { - RequestScope requestScope = new RequestScope(); - Key key = Key.get(Object.class); - Object value = new Object(); - try { - new MockUnit(Provider.class, Map.class) - .expect(unit -> { - Map scopedObjects = unit.get(Map.class); - requestScope.enter(scopedObjects); - when(scopedObjects.get(key)).thenReturn(null); - when(scopedObjects.containsKey(key)).thenReturn(false); - - when(scopedObjects.put(key, value)).thenReturn(null); - }) - .expect(unit -> { - Provider provider = unit.get(Provider.class); - when(provider.get()).thenReturn(value); - }) - .run(unit -> { - Object result = requestScope. scope(key, unit.get(Provider.class)).get(); - assertEquals(value, result); - }); - } finally { - requestScope.exit(); - } - } - - @SuppressWarnings({"unchecked", "rawtypes" }) - @Test - public void scopedNullValue() throws Exception { - RequestScope requestScope = new RequestScope(); - Key key = Key.get(Object.class); - try { - new MockUnit(Provider.class, Map.class) - .expect(unit -> { - Map scopedObjects = unit.get(Map.class); - requestScope.enter(scopedObjects); - when(scopedObjects.get(key)).thenReturn(null); - when(scopedObjects.containsKey(key)).thenReturn(true); - }) - .run(unit -> { - Object result = requestScope. scope(key, unit.get(Provider.class)).get(); - assertEquals(null, result); - }); - } finally { - requestScope.exit(); - } - } - - @SuppressWarnings({"unchecked", "rawtypes" }) - @Test - public void scopeExistingValue() throws Exception { - RequestScope requestScope = new RequestScope(); - Key key = Key.get(Object.class); - Object value = new Object(); - try { - new MockUnit(Provider.class, Map.class) - .expect(unit -> { - Map scopedObjects = unit.get(Map.class); - requestScope.enter(scopedObjects); - when(scopedObjects.get(key)).thenReturn(value); - }) - .run(unit -> { - Object result = requestScope. scope(key, unit.get(Provider.class)).get(); - assertEquals(value, result); - }); - } finally { - requestScope.exit(); - } - } - - @SuppressWarnings({"unchecked", "rawtypes" }) - @Test - public void circularScopedValue() throws Exception { - RequestScope requestScope = new RequestScope(); - Key key = Key.get(Object.class); - try { - new MockUnit(Provider.class, Map.class, CircularDependencyProxy.class) - .expect(unit -> { - Map scopedObjects = unit.get(Map.class); - requestScope.enter(scopedObjects); - when(scopedObjects.get(key)).thenReturn(null); - when(scopedObjects.containsKey(key)).thenReturn(false); - }) - .expect(unit -> { - Provider provider = unit.get(Provider.class); - when(provider.get()).thenReturn(unit.get(CircularDependencyProxy.class)); - }) - .run(unit -> { - Object result = requestScope. scope(key, unit.get(Provider.class)).get(); - assertEquals(unit.get(CircularDependencyProxy.class), result); - }); - } finally { - requestScope.exit(); - } - } - - @SuppressWarnings({"unchecked" }) - @Test(expected = OutOfScopeException.class) - public void outOfScope() throws Exception { - RequestScope requestScope = new RequestScope(); - Key key = Key.get(Object.class); - Object value = new Object(); - new MockUnit(Provider.class, Map.class) - .run(unit -> { - Object result = requestScope. scope(key, unit.get(Provider.class)).get(); - assertEquals(value, result); - }); - } -} diff --git a/jooby/src/test/java-excluded/org/jooby/internal/jetty/JettyHandlerTest.java b/jooby/src/test/java-excluded/org/jooby/internal/jetty/JettyHandlerTest.java deleted file mode 100644 index 6dfa36eb..00000000 --- a/jooby/src/test/java-excluded/org/jooby/internal/jetty/JettyHandlerTest.java +++ /dev/null @@ -1,472 +0,0 @@ -/* - * Copyright 2026 The Billing Project, LLC - * - * The Billing Project licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jooby.internal.jetty; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; -import static org.mockito.ArgumentMatchers.isA; - -import java.io.IOException; - -import javax.servlet.MultipartConfigElement; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.websocket.server.WebSocketServerFactory; -import org.jooby.servlet.ServletServletRequest; -import org.jooby.servlet.ServletServletResponse; -import org.jooby.spi.HttpHandler; -import org.jooby.spi.NativeWebSocket; -import org.jooby.test.MockUnit; -import org.jooby.test.MockUnit.Block; -import org.junit.Test; - -public class JettyHandlerTest { - - private Block wsStopTimeout = unit -> { - WebSocketServerFactory ws = unit.get(WebSocketServerFactory.class); - ws.setStopTimeout(30000L); - }; - - @Test - public void handleShouldSetMultipartConfig() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("Multipart/Form-Data"); - - request.setAttribute(eq(Request.MULTIPART_CONFIG_ELEMENT), - isA(MultipartConfigElement.class)); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(isA(ServletServletRequest.class), - isA(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }); - } - - @Test - public void handleShouldIgnoreMultipartConfig() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(isA(ServletServletRequest.class), - isA(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }); - } - - @Test - public void handleWsUpgrade() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class, NativeWebSocket.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - HttpServletRequest req = unit.get(HttpServletRequest.class); - HttpServletResponse rsp = unit.get(HttpServletResponse.class); - NativeWebSocket ws = unit.get(NativeWebSocket.class); - - WebSocketServerFactory factory = unit.get(WebSocketServerFactory.class); - - when(factory.isUpgradeRequest(req, rsp)).thenReturn(true); - - when(factory.acceptWebSocket(req, rsp)).thenReturn(true); - - when(req.getAttribute(JettyWebSocket.class.getName())).thenReturn(ws); - req.removeAttribute(JettyWebSocket.class.getName()); - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(unit.capture(ServletServletRequest.class), - unit.capture(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }, unit -> { - ServletServletRequest req = unit.captured(ServletServletRequest.class).get(0); - req.upgrade(NativeWebSocket.class); - }); - } - - @Test(expected = UnsupportedOperationException.class) - public void handleThrowUnsupportedOperationExceptionWhenWsIsMissing() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class, NativeWebSocket.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - HttpServletRequest req = unit.get(HttpServletRequest.class); - HttpServletResponse rsp = unit.get(HttpServletResponse.class); - - WebSocketServerFactory factory = unit.get(WebSocketServerFactory.class); - - when(factory.isUpgradeRequest(req, rsp)).thenReturn(true); - - when(factory.acceptWebSocket(req, rsp)).thenReturn(true); - - when(req.getAttribute(JettyWebSocket.class.getName())).thenReturn(null); - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(unit.capture(ServletServletRequest.class), - unit.capture(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }, unit -> { - ServletServletRequest req = unit.captured(ServletServletRequest.class).get(0); - req.upgrade(NativeWebSocket.class); - }); - } - - @Test(expected = UnsupportedOperationException.class) - public void handleThrowUnsupportedOperationExceptionOnNoWebSocketRequest() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class, NativeWebSocket.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - HttpServletRequest req = unit.get(HttpServletRequest.class); - HttpServletResponse rsp = unit.get(HttpServletResponse.class); - - WebSocketServerFactory factory = unit.get(WebSocketServerFactory.class); - - when(factory.isUpgradeRequest(req, rsp)).thenReturn(false); - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(unit.capture(ServletServletRequest.class), - unit.capture(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }, unit -> { - ServletServletRequest req = unit.captured(ServletServletRequest.class).get(0); - req.upgrade(NativeWebSocket.class); - }); - } - - @Test(expected = UnsupportedOperationException.class) - public void handleThrowUnsupportedOperationExceptionOnHankshakeRejection() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class, NativeWebSocket.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - HttpServletRequest req = unit.get(HttpServletRequest.class); - HttpServletResponse rsp = unit.get(HttpServletResponse.class); - - WebSocketServerFactory factory = unit.get(WebSocketServerFactory.class); - - when(factory.isUpgradeRequest(req, rsp)).thenReturn(true); - - when(factory.acceptWebSocket(req, rsp)).thenReturn(false); - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(unit.capture(ServletServletRequest.class), - unit.capture(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }, unit -> { - ServletServletRequest req = unit.captured(ServletServletRequest.class).get(0); - req.upgrade(NativeWebSocket.class); - }); - } - - @Test(expected = UnsupportedOperationException.class) - public void handleThrowUnsupportedOperationExceptionOnWrongType() throws Exception { - new MockUnit(Request.class, HttpHandler.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class, NativeWebSocket.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - HttpHandler dispatcher = unit.get(HttpHandler.class); - dispatcher.handle(unit.capture(ServletServletRequest.class), - unit.capture(ServletServletResponse.class)); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(unit.get(HttpHandler.class), unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }, unit -> { - ServletServletRequest req = unit.captured(ServletServletRequest.class).get(0); - req.upgrade(JettyHandlerTest.class); - }); - } - - @Test(expected = ServletException.class) - public void handleShouldReThrowServletException() throws Exception { - HttpHandler dispatcher = (request, response) -> { - throw new ServletException("intentional err"); - }; - new MockUnit(Request.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(false); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(dispatcher, unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }); - } - - @Test(expected = IOException.class) - public void handleShouldReThrowIOException() throws Exception { - HttpHandler dispatcher = (request, response) -> { - throw new IOException("intentional err"); - }; - new MockUnit(Request.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(false); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(dispatcher, unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }); - } - - @Test(expected = IllegalArgumentException.class) - public void handleShouldReThrowIllegalArgumentException() throws Exception { - HttpHandler dispatcher = (request, response) -> { - throw new IllegalArgumentException("intentional err"); - }; - new MockUnit(Request.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(false); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(dispatcher, unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }); - } - - @Test(expected = IllegalStateException.class) - public void handleShouldReThrowIllegalStateException() throws Exception { - HttpHandler dispatcher = (request, response) -> { - throw new Exception("intentional err"); - }; - new MockUnit(Request.class, WebSocketServerFactory.class, - HttpServletRequest.class, HttpServletResponse.class) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(true); - - when(request.getContentType()).thenReturn("application/json"); - }) - .expect(unit -> { - HttpServletRequest request = unit.get(HttpServletRequest.class); - - when(request.getPathInfo()).thenReturn("/"); - when(request.getContextPath()).thenReturn(""); - }) - .expect(unit -> { - Request request = unit.get(Request.class); - - request.setHandled(false); - }) - .expect(wsStopTimeout) - .run(unit -> { - new JettyHandler(dispatcher, unit.get(WebSocketServerFactory.class), - "target", -1) - .handle("/", unit.get(Request.class), - unit.get(HttpServletRequest.class), - unit.get(HttpServletResponse.class)); - }); - } -} diff --git a/jooby/src/test/java-excluded/org/jooby/internal/jetty/JettyServerTest.java b/jooby/src/test/java-excluded/org/jooby/internal/jetty/JettyServerTest.java deleted file mode 100644 index 5a324e7e..00000000 --- a/jooby/src/test/java-excluded/org/jooby/internal/jetty/JettyServerTest.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2026 The Billing Project, LLC - * - * The Billing Project licenses this file to you under the Apache License, version 2.0 - * (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.jooby.internal.jetty; - -import static org.easymock.EasyMock.eq; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.expectLastCall; -import static org.easymock.EasyMock.isA; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.util.Map; - -import javax.inject.Provider; -import javax.servlet.ServletContext; - -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.util.DecoratedObjectFactory; -import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.util.thread.ThreadPool; -import org.eclipse.jetty.websocket.api.WebSocketBehavior; -import org.eclipse.jetty.websocket.api.WebSocketPolicy; -import org.eclipse.jetty.websocket.server.WebSocketServerFactory; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; -import org.jooby.spi.HttpHandler; -import org.jooby.test.MockUnit; -import org.jooby.test.MockUnit.Block; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -import com.google.common.collect.ImmutableMap; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigException; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({JettyServer.class, Server.class, QueuedThreadPool.class, ServerConnector.class, - HttpConfiguration.class, HttpConnectionFactory.class, WebSocketPolicy.class, - WebSocketServerFactory.class }) -public class JettyServerTest { - - Map httpConfig = ImmutableMap. builder() - .put("HeaderCacheSize", "8k") - .put("RequestHeaderSize", "8k") - .put("ResponseHeaderSize", "8k") - .put("FileSizeThreshold", "16k") - .put("SendServerVersion", false) - .put("SendXPoweredBy", false) - .put("SendDateHeader", false) - .put("OutputBufferSize", "32k") - .put("BadOption", "bad") - .put("connector", ImmutableMap. builder() - .put("AcceptQueueSize", 0) - .put("SoLingerTime", -1) - .put("StopTimeout", "3s") - .put("IdleTimeout", "3s") - .build()) - .build(); - - Map ws = ImmutableMap. builder() - .put("MaxTextMessageSize", "64k") - .put("MaxTextMessageBufferSize", "32k") - .put("MaxBinaryMessageSize", "64k") - .put("MaxBinaryMessageBufferSize", "32kB") - .put("AsyncWriteTimeout", 60000) - .put("IdleTimeout", "5minutes") - .put("InputBufferSize", "4k") - .build(); - - Config config = ConfigFactory.empty() - .withValue("jetty.threads.MinThreads", ConfigValueFactory.fromAnyRef("1")) - .withValue("jetty.threads.MaxThreads", ConfigValueFactory.fromAnyRef("10")) - .withValue("jetty.threads.IdleTimeout", ConfigValueFactory.fromAnyRef("3s")) - .withValue("jetty.threads.Name", ConfigValueFactory.fromAnyRef("jetty task")) - .withValue("jetty.FileSizeThreshold", ConfigValueFactory.fromAnyRef(1024)) - .withValue("jetty.url.charset", ConfigValueFactory.fromAnyRef("UTF-8")) - .withValue("jetty.http", ConfigValueFactory.fromAnyRef(httpConfig)) - .withValue("jetty.ws", ConfigValueFactory.fromAnyRef(ws)) - .withValue("server.http.MaxRequestSize", ConfigValueFactory.fromAnyRef("200k")) - .withValue("server.http2.enabled", ConfigValueFactory.fromAnyRef(false)) - .withValue("application.port", ConfigValueFactory.fromAnyRef(6789)) - .withValue("application.host", ConfigValueFactory.fromAnyRef("0.0.0.0")) - .withValue("application.tmpdir", ConfigValueFactory.fromAnyRef("target")); - - private MockUnit.Block pool = unit -> { - QueuedThreadPool pool = unit.mockConstructor(QueuedThreadPool.class); - unit.registerMock(QueuedThreadPool.class, pool); - - pool.setMaxThreads(10); - pool.setMinThreads(1); - pool.setIdleTimeout(3000); - pool.setName("jetty task"); - }; - - private MockUnit.Block server = unit -> { - Server server = unit.constructor(Server.class) - .args(ThreadPool.class) - .build(unit.get(QueuedThreadPool.class)); - - ContextHandler ctx = unit.constructor(ContextHandler.class) - .build(); - ctx.setContextPath("/"); - ctx.setHandler(isA(JettyHandler.class)); - ctx.setAttribute(eq(DecoratedObjectFactory.ATTR), isA(DecoratedObjectFactory.class)); - expect(ctx.getServletContext()).andReturn(unit.get(ContextHandler.Context.class)); - - server.setStopAtShutdown(false); - server.setHandler(ctx); - server.start(); - server.join(); - server.stop(); - - unit.registerMock(Server.class, server); - - expect(server.getThreadPool()).andReturn(unit.get(QueuedThreadPool.class)).anyTimes(); - }; - - private MockUnit.Block httpConf = unit -> { - HttpConfiguration conf = unit.mockConstructor(HttpConfiguration.class); - conf.setOutputBufferSize(32768); - conf.setRequestHeaderSize(8192); - conf.setSendXPoweredBy(false); - conf.setHeaderCacheSize(8192); - conf.setSendServerVersion(false); - conf.setSendDateHeader(false); - conf.setResponseHeaderSize(8192); - - unit.registerMock(HttpConfiguration.class, conf); - }; - - private MockUnit.Block httpFactory = unit -> { - HttpConnectionFactory factory = unit.constructor(HttpConnectionFactory.class) - .args(HttpConfiguration.class) - .build(unit.get(HttpConfiguration.class)); - - unit.registerMock(HttpConnectionFactory.class, factory); - }; - - private MockUnit.Block connector = unit -> { - ServerConnector connector = unit.constructor(ServerConnector.class) - .args(Server.class, ConnectionFactory[].class) - .build(unit.get(HttpConnectionFactory.class)); - - connector.setSoLingerTime(-1); - connector.setIdleTimeout(3000); - connector.setStopTimeout(3000); - connector.setAcceptQueueSize(0); - connector.setPort(6789); - connector.setHost("0.0.0.0"); - - unit.registerMock(ServerConnector.class, connector); - - Server server = unit.get(Server.class); - server.addConnector(connector); - }; - - private Block wsPolicy = unit -> { - WebSocketPolicy policy = unit.constructor(WebSocketPolicy.class) - .args(WebSocketBehavior.class) - .build(WebSocketBehavior.SERVER); - - policy.setAsyncWriteTimeout(60000L); - policy.setMaxBinaryMessageSize(65536); - policy.setMaxBinaryMessageBufferSize(32000); - policy.setIdleTimeout(300000L); - policy.setMaxTextMessageSize(65536); - policy.setMaxTextMessageBufferSize(32768); - policy.setInputBufferSize(4096); - - unit.registerMock(WebSocketPolicy.class, policy); - }; - - private Block wsFactory = unit -> { - WebSocketServerFactory factory = unit.constructor(WebSocketServerFactory.class) - .args(ServletContext.class, WebSocketPolicy.class) - .build(unit.get(ContextHandler.Context.class), unit.get(WebSocketPolicy.class)); - - factory.setCreator(isA(WebSocketCreator.class)); - - factory.setStopTimeout(30000L); - - unit.registerMock(WebSocketServerFactory.class, factory); - }; - - @SuppressWarnings("unchecked") - @Test - public void startStopServer() throws Exception { - - new MockUnit(HttpHandler.class, Provider.class, ContextHandler.Context.class) - .expect(pool) - .expect(server) - .expect(httpConf) - .expect(httpFactory) - .expect(connector) - .expect(wsPolicy) - .expect(wsFactory) - .run(unit -> { - JettyServer server = new JettyServer(unit.get(HttpHandler.class), config, - unit.get(Provider.class)); - - assertNotNull(server.executor()); - server.start(); - assertTrue(server.executor().isPresent()); - server.join(); - server.stop(); - }); - } - - @SuppressWarnings("unchecked") - @Test(expected = IllegalArgumentException.class) - public void badOption() throws Exception { - - new MockUnit(HttpHandler.class, Provider.class) - .expect(unit -> { - QueuedThreadPool pool = unit.mockConstructor(QueuedThreadPool.class); - unit.registerMock(QueuedThreadPool.class, pool); - - pool.setMaxThreads(10); - expectLastCall().andThrow(new IllegalArgumentException("10")); - }) - .run(unit -> { - new JettyServer(unit.get(HttpHandler.class), config, unit.get(Provider.class)); - }); - } - - @SuppressWarnings("unchecked") - @Test(expected = ConfigException.BadValue.class) - public void badConfOption() throws Exception { - - new MockUnit(HttpHandler.class, Provider.class) - .run(unit -> { - new JettyServer(unit.get(HttpHandler.class), - config.withValue("jetty.threads.MinThreads", ConfigValueFactory.fromAnyRef("x")), - unit.get(Provider.class)); - }); - } - -} diff --git a/jooby/src/test/java/org/jooby/FileConfTest.java b/jooby/src/test/java/org/jooby/FileConfTest.java new file mode 100644 index 00000000..309d45b0 --- /dev/null +++ b/jooby/src/test/java/org/jooby/FileConfTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import org.junit.Test; + +public class FileConfTest { + + @Test + public void rootFile() throws Exception { + Path userDir = Files.createTempDirectory("jooby-fileconf-root"); + try { + writeFile(userDir.resolve("app.conf"), "source = root"); + + Config conf = withUserDir(userDir, () -> Jooby.fileConfig("app.conf")); + + assertEquals("root", conf.getString("source")); + } finally { + deleteRecursively(userDir); + } + } + + @Test + public void confFile() throws Exception { + Path userDir = Files.createTempDirectory("jooby-fileconf-conf"); + try { + Path confDir = Files.createDirectories(userDir.resolve("conf")); + writeFile(confDir.resolve("app.conf"), "source = conf"); + + Config conf = withUserDir(userDir, () -> Jooby.fileConfig("app.conf")); + + assertEquals("conf", conf.getString("source")); + } finally { + deleteRecursively(userDir); + } + } + + @Test + public void empty() throws Exception { + Path userDir = Files.createTempDirectory("jooby-fileconf-empty"); + try { + Config conf = withUserDir(userDir, () -> Jooby.fileConfig("app.conf")); + + assertTrue(conf.entrySet().isEmpty()); + } finally { + deleteRecursively(userDir); + } + } + + private void writeFile(final Path path, final String content) throws IOException { + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + } + + private Config withUserDir(final Path userDir, final ConfigSupplier supplier) throws Exception { + String original = System.getProperty("user.dir"); + System.setProperty("user.dir", userDir.toString()); + try { + return supplier.get(); + } finally { + if (original == null) { + System.clearProperty("user.dir"); + } else { + System.setProperty("user.dir", original); + } + } + } + + private void deleteRecursively(final Path root) throws IOException { + Files.walk(root) + .sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new IllegalStateException(e); + } + }); + } + + @FunctionalInterface + private interface ConfigSupplier { + Config get() throws Exception; + } +} diff --git a/jooby/src/test/java/org/jooby/LogbackConfTest.java b/jooby/src/test/java/org/jooby/LogbackConfTest.java new file mode 100644 index 00000000..cfd25639 --- /dev/null +++ b/jooby/src/test/java/org/jooby/LogbackConfTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby; + +import static org.junit.Assert.assertEquals; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import org.junit.Test; + +public class LogbackConfTest { + + @Test + public void withConfigFile() throws Exception { + Config conf = ConfigFactory.parseString("logback.configurationFile = logback.xml"); + assertEquals("logback.xml", Jooby.logback(conf)); + } + + @Test + public void rootFile() throws Exception { + Path userDir = Files.createTempDirectory("jooby-logback-root"); + try { + Config conf = ConfigFactory.empty(); + assertEquals("logback.xml", withUserDir(userDir, () -> Jooby.logback(conf))); + } finally { + deleteRecursively(userDir); + } + } + + @Test + public void rootFileFound() throws Exception { + Path userDir = Files.createTempDirectory("jooby-logback-root-found"); + try { + Path logback = userDir.resolve("logback.xml"); + writeFile(logback, ""); + + Config conf = ConfigFactory.empty(); + assertEquals(logback.toFile().getAbsolutePath(), withUserDir(userDir, () -> Jooby.logback(conf))); + } finally { + deleteRecursively(userDir); + } + } + + @Test + public void confFile() throws Exception { + Path userDir = Files.createTempDirectory("jooby-logback-conf"); + try { + Config conf = ConfigFactory.parseString("application.env = foo"); + assertEquals("logback.xml", withUserDir(userDir, () -> Jooby.logback(conf))); + } finally { + deleteRecursively(userDir); + } + } + + @Test + public void confFileFound() throws Exception { + Path userDir = Files.createTempDirectory("jooby-logback-conf-found"); + try { + Path confDir = Files.createDirectories(userDir.resolve("conf")); + Path envLogback = confDir.resolve("logback.foo.xml"); + writeFile(envLogback, ""); + + Config conf = ConfigFactory.parseString("application.env = foo"); + assertEquals(envLogback.toFile().getAbsolutePath(), + withUserDir(userDir, () -> Jooby.logback(conf))); + } finally { + deleteRecursively(userDir); + } + } + + private void writeFile(final Path path, final String content) throws IOException { + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + } + + private String withUserDir(final Path userDir, final StringSupplier supplier) throws Exception { + String original = System.getProperty("user.dir"); + System.setProperty("user.dir", userDir.toString()); + try { + return supplier.get(); + } finally { + if (original == null) { + System.clearProperty("user.dir"); + } else { + System.setProperty("user.dir", original); + } + } + } + + private void deleteRecursively(final Path root) throws IOException { + Files.walk(root) + .sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new IllegalStateException(e); + } + }); + } + + @FunctionalInterface + private interface StringSupplier { + String get() throws Exception; + } +} diff --git a/jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java b/jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java new file mode 100644 index 00000000..52d923ee --- /dev/null +++ b/jooby/src/test/java/org/jooby/handlers/CsrfHandlerTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby.handlers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.jooby.Err; +import org.jooby.Mutant; +import org.jooby.Request; +import org.jooby.Response; +import org.jooby.Route; +import org.jooby.Session; +import org.junit.Test; + +public class CsrfHandlerTest { + + @Test + public void invalidTokenDoesNotEchoCandidate() throws Throwable { + Request req = mock(Request.class); + Response rsp = mock(Response.class); + Route.Chain chain = mock(Route.Chain.class); + Session session = mock(Session.class); + Mutant tokenMutant = mock(Mutant.class); + Mutant headerMutant = mock(Mutant.class); + Mutant paramMutant = mock(Mutant.class); + + when(req.session()).thenReturn(session); + when(req.method()).thenReturn("POST"); + when(session.get("csrf")).thenReturn(tokenMutant); + when(tokenMutant.toOptional()).thenReturn(Optional.of("real-token")); + when(req.header("csrf")).thenReturn(headerMutant); + when(headerMutant.toOptional()).thenReturn(Optional.empty()); + when(req.param("csrf")).thenReturn(paramMutant); + when(paramMutant.toOptional()).thenReturn(Optional.of("attacker-token")); + when(req.set(anyString(), any())).thenReturn(req); + + CsrfHandler handler = new CsrfHandler(); + try { + handler.handle(req, rsp, chain); + fail("Expected Err to be thrown"); + } catch (Err err) { + assertEquals(403, err.statusCode()); + assertTrue("Error message should contain 'Invalid CSRF token'", + err.getMessage().contains("Invalid CSRF token")); + assertTrue("Error message must not contain the candidate token", + !err.getMessage().contains("attacker-token")); + } + } + + @Test + public void validTokenPassesThrough() throws Throwable { + Request req = mock(Request.class); + Response rsp = mock(Response.class); + Route.Chain chain = mock(Route.Chain.class); + Session session = mock(Session.class); + Mutant tokenMutant = mock(Mutant.class); + Mutant headerMutant = mock(Mutant.class); + + String token = "valid-token"; + + when(req.session()).thenReturn(session); + when(req.method()).thenReturn("POST"); + when(session.get("csrf")).thenReturn(tokenMutant); + when(tokenMutant.toOptional()).thenReturn(Optional.of(token)); + when(req.header("csrf")).thenReturn(headerMutant); + when(headerMutant.toOptional()).thenReturn(Optional.of(token)); + when(req.set(anyString(), any())).thenReturn(req); + + CsrfHandler handler = new CsrfHandler(); + handler.handle(req, rsp, chain); + + verify(chain).next(req, rsp); + } + + @Test + public void getRequestSkipsTokenVerification() throws Throwable { + Request req = mock(Request.class); + Response rsp = mock(Response.class); + Route.Chain chain = mock(Route.Chain.class); + Session session = mock(Session.class); + Mutant tokenMutant = mock(Mutant.class); + + when(req.session()).thenReturn(session); + when(req.method()).thenReturn("GET"); + when(session.get("csrf")).thenReturn(tokenMutant); + when(tokenMutant.toOptional()).thenReturn(Optional.of("some-token")); + when(req.set(anyString(), any())).thenReturn(req); + + CsrfHandler handler = new CsrfHandler(); + handler.handle(req, rsp, chain); + + verify(chain).next(req, rsp); + } + + @Test + public void missingTokenThrowsForbidden() throws Throwable { + Request req = mock(Request.class); + Response rsp = mock(Response.class); + Route.Chain chain = mock(Route.Chain.class); + Session session = mock(Session.class); + Mutant tokenMutant = mock(Mutant.class); + Mutant headerMutant = mock(Mutant.class); + Mutant paramMutant = mock(Mutant.class); + + when(req.session()).thenReturn(session); + when(req.method()).thenReturn("POST"); + when(session.get("csrf")).thenReturn(tokenMutant); + when(tokenMutant.toOptional()).thenReturn(Optional.of("real-token")); + when(req.header("csrf")).thenReturn(headerMutant); + when(headerMutant.toOptional()).thenReturn(Optional.empty()); + when(req.param("csrf")).thenReturn(paramMutant); + when(paramMutant.toOptional()).thenReturn(Optional.empty()); + when(req.set(anyString(), any())).thenReturn(req); + + CsrfHandler handler = new CsrfHandler(); + try { + handler.handle(req, rsp, chain); + fail("Expected Err to be thrown"); + } catch (Err err) { + assertEquals(403, err.statusCode()); + assertTrue("Error message should contain 'Invalid CSRF token'", + err.getMessage().contains("Invalid CSRF token")); + } + } +} diff --git a/jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java b/jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java new file mode 100644 index 00000000..cd773ca7 --- /dev/null +++ b/jooby/src/test/java/org/jooby/handlers/SSIHandlerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby.handlers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class SSIHandlerTest { + + @Test + public void missingResourceIncludesPathInMessage() throws Exception { + SSIHandler handler = new SSIHandler(); + + Method fileMethod = SSIHandler.class.getDeclaredMethod("file", String.class); + fileMethod.setAccessible(true); + + try { + fileMethod.invoke(handler, "/nonexistent/resource.html"); + fail("Expected NoSuchElementException"); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + assertTrue("Expected NoSuchElementException but got " + cause.getClass(), + cause instanceof NoSuchElementException); + String message = cause.getMessage(); + assertTrue("Exception message should contain the resource path, got: " + message, + message.contains("/nonexistent/resource.html")); + } + } + + @Test + public void existingResourceReturnsContent() throws Exception { + SSIHandler handler = new SSIHandler(); + + Method fileMethod = SSIHandler.class.getDeclaredMethod("file", String.class); + fileMethod.setAccessible(true); + + // Use a resource that definitely exists on the classpath + String result = (String) fileMethod.invoke(handler, "/org/jooby/mime.properties"); + assertTrue("Should return non-empty content", result != null && !result.isEmpty()); + } +} diff --git a/jooby/src/test/java/org/jooby/internal/RequestScopeTest.java b/jooby/src/test/java/org/jooby/internal/RequestScopeTest.java new file mode 100644 index 00000000..e992c89a --- /dev/null +++ b/jooby/src/test/java/org/jooby/internal/RequestScopeTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import com.google.inject.Key; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; + +public class RequestScopeTest { + + @Test + public void enter() { + RequestScope requestScope = new RequestScope(); + requestScope.enter(Collections.emptyMap()); + requestScope.exit(); + } + + @Test + public void scopedValue() { + RequestScope requestScope = new RequestScope(); + Key key = Key.get(Object.class); + Object value = new Object(); + Map scopedObjects = new HashMap<>(); + Provider provider = () -> value; + try { + requestScope.enter(scopedObjects); + + Object result = requestScope.scope(key, provider).get(); + + assertSame(value, result); + assertSame(value, scopedObjects.get(key)); + } finally { + requestScope.exit(); + } + } + + @Test + public void scopedNullValue() { + RequestScope requestScope = new RequestScope(); + Key key = Key.get(Object.class); + Map scopedObjects = new HashMap<>(); + Provider provider = () -> { + throw new AssertionError("provider should not be called"); + }; + try { + requestScope.enter(scopedObjects); + scopedObjects.put(key, null); + + Object result = requestScope.scope(key, provider).get(); + + assertNull(result); + } finally { + requestScope.exit(); + } + } + + @Test + public void scopeExistingValue() { + RequestScope requestScope = new RequestScope(); + Key key = Key.get(Object.class); + Object value = new Object(); + Map scopedObjects = new HashMap<>(); + Provider provider = () -> { + throw new AssertionError("provider should not be called"); + }; + try { + requestScope.enter(scopedObjects); + scopedObjects.put(key, value); + + Object result = requestScope.scope(key, provider).get(); + + assertSame(value, result); + } finally { + requestScope.exit(); + } + } + + @Test + public void circularScopedValue() { + RequestScope requestScope = new RequestScope(); + Key key = Key.get(Object.class); + Map scopedObjects = new HashMap<>(); + Object value = circularProxy(); + Provider provider = () -> value; + try { + requestScope.enter(scopedObjects); + + Object result = requestScope.scope(key, provider).get(); + + assertSame(value, result); + assertTrue(com.google.inject.Scopes.isCircularProxy(result)); + assertEquals(Collections.emptyMap(), scopedObjects); + } finally { + requestScope.exit(); + } + } + + @SuppressWarnings("unchecked") + @Test(expected = OutOfScopeException.class) + public void outOfScope() { + RequestScope requestScope = new RequestScope(); + requestScope.scope(Key.get(Object.class), Object::new).get(); + } + + private static Object circularProxy() { + try { + Class handlerType = Class.forName("com.google.inject.internal.DelegatingInvocationHandler"); + Constructor constructor = handlerType.getDeclaredConstructor(); + constructor.setAccessible(true); + Object handler = constructor.newInstance(); + + Class bytecodeGenType = Class.forName("com.google.inject.internal.BytecodeGen"); + Method newCircularProxy = + bytecodeGenType.getDeclaredMethod("newCircularProxy", Class.class, java.lang.reflect.InvocationHandler.class); + newCircularProxy.setAccessible(true); + return newCircularProxy.invoke(null, CircularContract.class, handler); + } catch (ReflectiveOperationException x) { + throw new AssertionError(x); + } + } + + private interface CircularContract { + } +} diff --git a/jooby/src/test/java/org/jooby/internal/jetty/JettyHandlerTest.java b/jooby/src/test/java/org/jooby/internal/jetty/JettyHandlerTest.java new file mode 100644 index 00000000..5011e3bb --- /dev/null +++ b/jooby/src/test/java/org/jooby/internal/jetty/JettyHandlerTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby.internal.jetty; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.jooby.Sse; +import org.jooby.servlet.ServletServletRequest; +import org.jooby.spi.HttpHandler; +import org.jooby.spi.NativePushPromise; +import org.jooby.spi.NativeRequest; +import org.jooby.spi.NativeResponse; +import org.junit.Test; +import org.mockito.Mockito; + +public class JettyHandlerTest { + + @Test + public void handleShouldSetMultipartConfig() throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + AtomicReference capturedRequest = new AtomicReference<>(); + AtomicReference capturedResponse = new AtomicReference<>(); + + when(baseRequest.getContentType()).thenReturn("Multipart/Form-Data"); + + HttpHandler dispatcher = (req, rsp) -> { + capturedRequest.set(req); + capturedResponse.set(rsp); + }; + + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + + verify(baseRequest).setHandled(true); + verify(baseRequest).setAttribute(eq("org.eclipse.jetty.multipartConfig"), + any(MultipartConfigElement.class)); + assertTrue(capturedRequest.get() instanceof ServletServletRequest); + assertTrue(capturedResponse.get() instanceof JettyResponse); + } + + @Test + public void handleShouldIgnoreMultipartConfig() throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + + when(baseRequest.getContentType()).thenReturn("application/json"); + + HttpHandler dispatcher = (req, rsp) -> { + }; + + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + + verify(baseRequest).setHandled(true); + verify(baseRequest, never()).setAttribute(eq("org.eclipse.jetty.multipartConfig"), + any(MultipartConfigElement.class)); + } + + @Test + public void handleShouldSupportSseUpgrade() throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + AtomicReference capturedRequest = new AtomicReference<>(); + + when(baseRequest.getContentType()).thenReturn("application/json"); + + HttpHandler dispatcher = (req, rsp) -> { + capturedRequest.set((ServletServletRequest) req); + }; + + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + + assertTrue(capturedRequest.get().upgrade(Sse.class) instanceof JettySse); + } + + @Test + public void handleShouldSupportPushPromiseUpgrade() throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + AtomicReference capturedRequest = new AtomicReference<>(); + + when(baseRequest.getContentType()).thenReturn("application/json"); + + HttpHandler dispatcher = (req, rsp) -> { + capturedRequest.set((ServletServletRequest) req); + }; + + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + + assertTrue(capturedRequest.get().upgrade(NativePushPromise.class) instanceof JettyPush); + } + + @Test(expected = UnsupportedOperationException.class) + public void handleShouldRejectUnsupportedUpgradeType() throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + AtomicReference capturedRequest = new AtomicReference<>(); + + when(baseRequest.getContentType()).thenReturn("application/json"); + + HttpHandler dispatcher = (req, rsp) -> { + capturedRequest.set((ServletServletRequest) req); + }; + + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + + capturedRequest.get().upgrade(JettyHandlerTest.class); + } + + @Test(expected = ServletException.class) + public void handleShouldReThrowServletException() throws Exception { + shouldPropagate(new ServletException("intentional err")); + } + + @Test(expected = IOException.class) + public void handleShouldReThrowIOException() throws Exception { + shouldPropagate(new IOException("intentional err")); + } + + @Test(expected = IllegalArgumentException.class) + public void handleShouldReThrowRuntimeException() throws Exception { + shouldPropagate(new IllegalArgumentException("intentional err")); + } + + @Test + public void handleShouldWrapCheckedThrowable() throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + + when(baseRequest.getContentType()).thenReturn("application/json"); + + HttpHandler dispatcher = (req, rsp) -> { + throw new Exception("intentional err"); + }; + + try { + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + } catch (IllegalStateException e) { + assertEquals("intentional err", e.getCause().getMessage()); + verify(baseRequest).setHandled(false); + return; + } + throw new AssertionError("Expected IllegalStateException"); + } + + private void shouldPropagate(final Exception cause) throws Exception { + Request baseRequest = Mockito.mock(Request.class); + HttpServletRequest request = newRequest(); + Response response = Mockito.mock(Response.class); + + when(baseRequest.getContentType()).thenReturn("application/json"); + + HttpHandler dispatcher = (req, rsp) -> { + throw cause; + }; + + try { + new JettyHandler(dispatcher, "target", -1).handle("/", baseRequest, request, response); + } finally { + verify(baseRequest).setHandled(false); + } + } + + private HttpServletRequest newRequest() { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + when(request.getPathInfo()).thenReturn("/"); + when(request.getContextPath()).thenReturn(""); + return request; + } +} diff --git a/jooby/src/test/java/org/jooby/internal/jetty/JettyServerTest.java b/jooby/src/test/java/org/jooby/internal/jetty/JettyServerTest.java new file mode 100644 index 00000000..5f6eeeaa --- /dev/null +++ b/jooby/src/test/java/org/jooby/internal/jetty/JettyServerTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby.internal.jetty; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Provider; +import javax.net.ssl.SSLContext; + +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.jooby.spi.HttpHandler; +import org.junit.Test; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; + +public class JettyServerTest { + + @Test + public void shouldBuildHttpServerAndExposeExecutor() throws Exception { + JettyServer jetty = new JettyServer(handler(), config(false, false), sslProvider()); + + Server server = server(jetty); + ServerConnector connector = (ServerConnector) server.getConnectors()[0]; + ContextHandler context = (ContextHandler) server.getHandler(); + + assertTrue(jetty.executor().isPresent()); + assertSame(server.getThreadPool(), jetty.executor().get()); + assertEquals(1, server.getConnectors().length); + assertEquals(6789, connector.getPort()); + assertEquals("0.0.0.0", connector.getHost()); + assertFactoryTypes(connector, HttpConnectionFactory.class); + assertNotNull(context); + assertEquals("/", context.getContextPath()); + assertTrue(context.getHandler() instanceof JettyHandler); + assertEquals("UTF-8", System.getProperty("org.eclipse.jetty.util.UrlEncoded.charset")); + assertEquals("204800", System.getProperty("org.eclipse.jetty.server.Request.maxFormContentSize")); + } + + @Test + public void shouldBuildCleartextHttp2ConnectorWhenConfigured() throws Exception { + JettyServer jetty = new JettyServer(handler(), config(false, true), sslProvider()); + + ServerConnector connector = (ServerConnector) server(jetty).getConnectors()[0]; + + assertEquals(1, server(jetty).getConnectors().length); + assertFactoryTypes(connector, HttpConnectionFactory.class, HTTP2CServerConnectionFactory.class); + } + + @Test + public void shouldBuildSecureConnectorWhenConfigured() throws Exception { + AtomicInteger sslRequests = new AtomicInteger(); + Provider sslProvider = () -> { + try { + sslRequests.incrementAndGet(); + return SSLContext.getDefault(); + } catch (Exception x) { + throw new IllegalStateException(x); + } + }; + + JettyServer jetty = new JettyServer(handler(), config(true, false), sslProvider); + + Connector[] connectors = server(jetty).getConnectors(); + ServerConnector http = Arrays.stream(connectors) + .map(ServerConnector.class::cast) + .filter(it -> it.getConnectionFactory(HttpConnectionFactory.class) != null + && it.getConnectionFactory(SslConnectionFactory.class) == null) + .findFirst() + .orElseThrow(AssertionError::new); + ServerConnector https = Arrays.stream(connectors) + .map(ServerConnector.class::cast) + .filter(it -> it.getConnectionFactory(SslConnectionFactory.class) != null) + .findFirst() + .orElseThrow(AssertionError::new); + + assertEquals(2, connectors.length); + assertEquals(6789, http.getPort()); + assertEquals(7443, https.getPort()); + assertFactoryTypes(http, HttpConnectionFactory.class); + assertFactoryTypes(https, SslConnectionFactory.class, HttpConnectionFactory.class); + assertEquals(1, sslRequests.get()); + } + + @Test + public void shouldStartAndStopServer() throws Exception { + JettyServer jetty = new JettyServer(handler(), + config(false, false).withValue("application.port", ConfigValueFactory.fromAnyRef(0)), + sslProvider()); + + jetty.start(); + try { + assertTrue(server(jetty).isStarted()); + } finally { + jetty.stop(); + } + assertTrue(server(jetty).isStopped()); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldPropagateSetterFailures() throws Throwable { + JettyServer jetty = new JettyServer(handler(), config(false, false), sslProvider()); + Method conf = JettyServer.class.getDeclaredMethod("conf", Object.class, Config.class, String.class); + conf.setAccessible(true); + + try { + conf.invoke(jetty, new ThrowingOption(), ConfigFactory.parseMap(Map.of("MaxThreads", 10)), "test"); + } catch (InvocationTargetException x) { + throw x.getCause(); + } + } + + @Test(expected = ConfigException.BadValue.class) + public void shouldRejectBadThreadConfig() { + new JettyServer(handler(), + config(false, false).withValue("jetty.threads.MinThreads", + ConfigValueFactory.fromAnyRef("x")), + sslProvider()); + } + + private static HttpHandler handler() { + return mock(HttpHandler.class); + } + + private static Provider sslProvider() { + return () -> { + try { + return SSLContext.getDefault(); + } catch (Exception x) { + throw new IllegalStateException(x); + } + }; + } + + private static Config config(final boolean securePort, final boolean http2) { + Map source = new LinkedHashMap<>(); + source.put("jetty.threads.MinThreads", "1"); + source.put("jetty.threads.MaxThreads", "10"); + source.put("jetty.threads.IdleTimeout", "3s"); + source.put("jetty.threads.Name", "jetty task"); + source.put("jetty.FileSizeThreshold", 1024); + source.put("jetty.url.charset", "UTF-8"); + source.put("jetty.http.HeaderCacheSize", "8k"); + source.put("jetty.http.RequestHeaderSize", "8k"); + source.put("jetty.http.ResponseHeaderSize", "8k"); + source.put("jetty.http.SendServerVersion", false); + source.put("jetty.http.SendXPoweredBy", false); + source.put("jetty.http.SendDateHeader", false); + source.put("jetty.http.OutputBufferSize", "32k"); + source.put("jetty.http.connector.AcceptQueueSize", 0); + source.put("jetty.http.connector.IdleTimeout", "3s"); + source.put("server.http.MaxRequestSize", "200k"); + source.put("server.http2.enabled", http2); + source.put("application.port", 6789); + source.put("application.host", "0.0.0.0"); + source.put("application.tmpdir", "target"); + if (securePort) { + source.put("application.securePort", 7443); + } + return ConfigFactory.parseMap(source); + } + + private static Server server(final JettyServer jetty) throws Exception { + Field field = JettyServer.class.getDeclaredField("server"); + field.setAccessible(true); + return (Server) field.get(jetty); + } + + private static void assertFactoryTypes(final ServerConnector connector, + final Class... expectedTypes) { + ConnectionFactory[] factories = connector.getConnectionFactories().toArray(new ConnectionFactory[0]); + assertEquals(expectedTypes.length, factories.length); + for (int i = 0; i < expectedTypes.length; i++) { + assertTrue(expectedTypes[i].isInstance(factories[i])); + } + } + + public static class ThrowingOption { + public void setMaxThreads(final Integer value) { + throw new IllegalArgumentException(String.valueOf(value)); + } + } +} diff --git a/jooby/src/test/java/org/jooby/test/SseFeature.java b/jooby/src/test/java/org/jooby/test/SseFeature.java new file mode 100644 index 00000000..a3f64c80 --- /dev/null +++ b/jooby/src/test/java/org/jooby/test/SseFeature.java @@ -0,0 +1,60 @@ +/* + * Copyright 2026 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jooby.test; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; + +import org.jooby.Jooby; +import org.jooby.MediaType; +import org.junit.runner.RunWith; + +/** + * Internal use only. + * + * @author edgar + */ +@RunWith(JoobySuite.class) +public abstract class SseFeature extends Jooby { + + protected int port; + + public String sse(final String path, final int count) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:" + port + path)) + .header("Content-Type", MediaType.sse.name()) + .header("last-event-id", Integer.toString(count)) + .GET() + .build(); + + HttpResponse response = + HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream()); + + assertEquals(200, response.statusCode()); + assertEquals("close", response.headers().firstValue("Connection").orElse("").toLowerCase()); + assertEquals("text/event-stream; charset=utf-8", + response.headers().firstValue("Content-Type").orElse("").toLowerCase()); + + try (InputStream body = response.body()) { + return new String(body.readAllBytes(), StandardCharsets.UTF_8); + } + } +} diff --git a/killbill-jooby-todo.md b/killbill-jooby-todo.md new file mode 100644 index 00000000..165ee872 --- /dev/null +++ b/killbill-jooby-todo.md @@ -0,0 +1,317 @@ +# Kill Bill Modernization — Full Roadmap + +> This document covers both the `killbill-jooby` module (a source fork of Jooby 1.6.9) and +> the broader Jakarta EE / JDK modernization. The sections are ordered by execution sequence — +> each depends on the ones before it. Each section is a **separate branch and PR**. +> Do NOT bundle them. + +--- + +## Phase 0 — Current State Assessment ✅ + +- The codebase targets **JDK 11** as baseline (`project.build.targetJdk=11` via `killbill-oss-parent:0.146.63`). +- 90 Java source files use `javax.*` imports — zero `jakarta.*` imports exist anywhere. +- Maven coordinates already use transitional `jakarta.*` groupIds for some dependencies, but these still ship `javax.*` packages: + - `jakarta.servlet:jakarta.servlet-api` in `skeleton`, `metrics` (code uses `javax.servlet.*`). + - `jakarta.ws.rs:jakarta.ws.rs-api` in `skeleton` (code uses `javax.ws.rs.*`). + - `jakarta.xml.bind:jakarta.xml.bind-api` in `automaton`, `xmlloader` (code uses `javax.xml.bind.*`). + - `jakarta.activation:jakarta.activation-api` in `automaton`, `xmlloader`. +- `javax.inject:javax.inject` is still the Maven artifact in 4 modules: `skeleton` (5 files), `queue` (4 files), `jdbi` (2 files), `metrics` (1 file). +- Guice version is **5.1.0** (managed by `killbill-oss-parent`; supports `javax.inject` only). +- Jersey version is **2.39.1** (supports `javax.ws.rs` only; Jersey 3.x requires `jakarta.ws.rs`). +- `javax.servlet` imports span 12 files: `skeleton` (8 files), `metrics` (4 files). +- `javax.ws.rs` imports span 6 files: `skeleton` only. +- `javax.xml.bind` imports span 14 files: `automaton` (7 files), `xmlloader` (7 files). +- `javax.annotation.Nullable`/`@Nonnull` (from `jsr305`) is used in 35 files across 8 modules. + - This does NOT need migration — `jsr305` is not part of the Jakarta namespace transition. +- `javax.sql` is used in 22 files across 5 modules — this is core JDK, NOT part of Jakarta migration. +- Jooby 1.6.9 uses `javax.*` namespace, which matches the current codebase state. + - Jooby 2.x/3.x uses `jakarta.*` — that upgrade happens later in Phase 4. +- `killbill-commons` already contains vendored forks: `jdbi` (fork of jDBI 2.62) and `config-magic` (fork of config-magic 0.17). + - `killbill-jooby` follows the same forking pattern. + +--- + +# Phase 1 — Fork Jooby 1.6.9 into killbill-jooby ✅ + +> Fork the Jooby 1.6.9 source code into `killbill-commons` as a vendored module, +> following the same pattern as `killbill-jdbi` and `killbill-config-magic`. +> No application-level logic — just the forked library repackaged under Kill Bill Maven coordinates. + +## 1. Identify Upstream Source Scope ✅ + +- Fork 5 upstream Jooby 1.6.9 repos/modules: `jooby` (core), `jooby-servlet`, `jooby-jetty`, `jooby-jackson`, and `funzy`. +- **Exclude `jooby-netty`** — SSE (`org.jooby.Sse`) is defined in core as an abstract class; the server runtime (Jetty/Netty) provides the implementation via the `org.jooby.spi.*` SPI, so SSE works fine on Jetty without Netty. +- Netty would pull 8+ artifacts (`netty-transport`, `codec`, `codec-http`, `codec-http2`, `handler`, `common`, `resolver`, `epoll`) plus Javassist and platform-specific profiles — too costly and unnecessary since Kill Bill uses Jetty. +- Note the dependency chain: `jooby-jetty` → `jooby-servlet` → `jooby` (core); all three are required. +- `funzy` is inlined into the fork (3 source + 3 test files under `org.jooby.funzy`) — it's a separate repo (`jooby-project/funzy`, commit `728d743ca348f6f12430ec8735057cf6a1687c0c`) but only 3 classes with zero deps, deeply used in 24 core files. +- Upstream Jooby commit SHA: `85a50d5e894d14068b2e90a0601481cf52a0abec` (tag `v1.6.9`). +- Upstream funzy commit SHA: `728d743ca348f6f12430ec8735057cf6a1687c0c`. +- Inventory: 172 main Java files, 125 test Java files, 6 main resources, 8 test resources, 18 packages — zero conflicts. + +## 2. Project Scaffolding ✅ + +- Created the `jooby/` directory under `killbill-commons` root as a **single flat module** (packaging `jar`), following the `config-magic`/`jdbi` pattern. +- All 5 upstream repos/modules (`jooby` core, `jooby-servlet`, `jooby-jetty`, `jooby-jackson`, `funzy`) merge into this one artifact. +- Created `jooby/pom.xml` with parent `org.kill-bill.commons:killbill-commons:0.27.0-SNAPSHOT` and artifactId `killbill-jooby`. +- Added `jooby` to the `` list in the root `pom.xml`. +- Added `killbill-jooby` to the root ``. +- This follows the same pattern as `config-magic` and `jdbi` (single flat module). + +## 3. Copy Upstream Source ✅ + +- Copied `jooby/src/` into `jooby/src/`, preserving the original `org.jooby` package structure (core classes). +- Copied `modules/jooby-servlet/src/` into `jooby/src/`, preserving the original `org.jooby.servlet` package structure. +- Copied `modules/jooby-jetty/src/` into `jooby/src/`, preserving the original `org.jooby.jetty` package structure. +- Copied `modules/jooby-jackson/src/` into `jooby/src/`, preserving the original `org.jooby.json` package structure. +- Copied `funzy/src/` into `jooby/src/`, preserving the original `org.jooby.funzy` package structure (3 source + 3 test files). +- All upstream modules use different packages — merging into one `src/` tree caused no package or class name conflicts. +- Copied upstream resource files (`META-INF/web.xml`, `jooby.conf`, `server.conf`, SSL certs, `mime.properties`) into `src/main/resources/`. +- Preserved original copyright/license headers in all copied files (Apache License 2.0). + +## 4. Adapt pom.xml Dependencies ✅ + +- Complete POM written from scratch — see `jooby/CHANGES.md` for the full dependency version mapping table. +- `guice-multibindings` removed (merged into core Guice since 4.2). +- `funzy` removed as external dep (inlined into `org.jooby.funzy`). +- `javax.servlet-api` → `jakarta.servlet:jakarta.servlet-api:4.0.4` (transitional, still ships `javax.servlet`). +- Jetty upgraded from 9.4.24 → 10.0.16; `websocket-server` → `websocket-jetty-server`; `jetty-alpn-server` added. +- ASM shade plugin preserved (`org.objectweb.asm` → `org.jooby.internal.asm`). +- `jakarta.annotation-api:1.3.5` added for `@PostConstruct`/`@PreDestroy`. +- PowerMock removed (obsolete for modern JDKs). +- All other deps aligned to Kill Bill managed versions (Jackson, Guava, SLF4J, Typesafe Config). + +## 5. Build Verification (Initial Fork) ✅ + +- `mvn clean compile -pl jooby` passes with zero errors (8 deprecation warnings from upstream code). +- Fixed 4 Jetty 9→10 API incompatibilities: `JettyServer.java`, `JettyHandler.java`, `JettyPush.java`, `JettyResponse.java` — all documented in `CHANGES.md`. +- HTTP/2 Server Push (`PushBuilder`) stubbed as no-op (removed from Jetty 10 / HTTP/2 spec). +- WebSocket server factory removed from `JettyHandler`/`JettyServer` (Jetty 10 restructured the WebSocket API). +- `SslContextFactory` → `SslContextFactory.Server` (made abstract in Jetty 10). +- All 297 Java files have Kill Bill standard license headers (replacing 202-line Apache headers or prepending to headerless files). +- The remaining test-tree migration work was completed in later Phase 1 steps. + +## 6. Configure Upstream Test Handling ✅ + +- Upstream tests use **JUnit 4** (116 `@Test` imports, 35 `@RunWith` usages). +- 76 test files depended on PowerMock (67 via `MockUnit.java`, 4 test utilities, 5 transitive) and were moved temporarily to `src/test/java-excluded/` during the migration. +- 3 test utility files (`Client.java`, `ServerFeature.java`, `SseFeature.java`) were part of the temporary `java-excluded/` set during the migration; all three are now restored under `src/test/java/`. +- Jooby tests now run in the default Maven lifecycle with `surefire-junit47` and `reuseForks=false`. +- Current active Jooby test baseline: `124` Java files in `src/test/java`, `108` runnable test classes, `923` tests, `src/test/java-excluded/` empty. +- `mvn clean install -pl jooby` passes all checks (dependency:analyze, SpotBugs, Apache RAT). +- SpotBugs exclude filter (`spotbugs-exclude.xml`) suppresses the remaining upstream findings after triage. +- Apache RAT exclusions added for resource files and the temporary migration layout. +- Removed `spotbugs-annotations` dependency (no upstream source uses `@SuppressFBWarnings`). +- Removed `websocket-jetty-server` dependency (WebSocket factory code removed from Jetty adapter). +- Added explicit deps for `websocket-jetty-api`, `jetty-io`, `jetty-util`, `javax.inject` (transitive but used directly). + +## 7. Migrate EasyMock + PowerMock to Mockito ✅ + +- Full migration to **Mockito 5** (`mockito-core:5.3.1`, managed by `killbill-oss-parent`) is complete; EasyMock and PowerMock are removed from the active test tree. +- `MockUnit.java` was rewritten around Mockito APIs, including native static and construction mocking support. +- The temporary `src/test/java-excluded/` test set has been fully reintegrated into the active test tree. + +## 8. Documentation ✅ + +- `jooby/README.md` created — documents upstream sources, forked modules table, git diff command, and why the fork exists. +- `jooby/CHANGES.md` created — full audit of all deviations from upstream (license headers, Java source changes, dependency version mapping, structural changes). +- All license headers updated to Kill Bill standard (single copyright line, non-javadoc comment style). +- `jooby-netty` exclusion documented in both README and CHANGES.md. + +## 9. SpotBugs & Static Analysis ✅ + +- Ran SpotBugs on the forked source and triaged the findings instead of keeping a blanket suppression. +- Added targeted `spotbugs-exclude.xml` rules for the remaining upstream findings and wired the filter into the module build. +- Fixed one real upstream bug during triage: `Response.Forwarding.setResetHeadersOnError()` now delegates to `rsp` instead of recursing on `this`. +- Cleaned up additional CI/static-analysis fallout: removed the obsolete `Issue1087.java` / `jackson-annotations` dependency path and fixed the CodeQL ReDoS findings in `RoutePattern.java` and `PemReader.java`. + +## 10. Publish as SNAPSHOT ✅ + +- Confirmed `mvn clean install` from the repository root succeeds in a clean worktree and installs `killbill-jooby` to the local Maven repository. +- Verified downstream consumers can depend on `org.kill-bill.commons:killbill-jooby:0.27.0-SNAPSHOT`. +- Confirmed the packaged artifact contains the expected `org.jooby.*` classes from the merged Jooby core, servlet, jetty, jackson, and funzy sources. + +## 11. Restore `FileConfTest` ✅ + +- Source: `src/test/java-excluded/org/jooby/FileConfTest.java` +- Target path: `src/test/java/org/jooby/FileConfTest.java` +- Restored as a real filesystem-based test in `src/test/java/org/jooby/FileConfTest.java`. +- Re-review target: current `Jooby.fileConfig(String)` behavior in `src/main/java/org/jooby/Jooby.java`. + +## 12. Restore `LogbackConfTest` ✅ + +- Source: `src/test/java-excluded/org/jooby/LogbackConfTest.java` +- Target path: `src/test/java/org/jooby/LogbackConfTest.java` +- Restored as a real filesystem/config-driven test in `src/test/java/org/jooby/LogbackConfTest.java`. +- Re-review target: current `Jooby.logback(Config)` behavior in `src/main/java/org/jooby/Jooby.java`. + +## 13. Restore `RequestScopeTest` ✅ + +- Source: `src/test/java-excluded/org/jooby/internal/RequestScopeTest.java` +- Target path: `src/test/java/org/jooby/internal/RequestScopeTest.java` +- Restored as a direct behavior test in `src/test/java/org/jooby/internal/RequestScopeTest.java`. +- Re-review target: `src/main/java/org/jooby/internal/RequestScope.java`. + +## 14. Rewrite `JettyHandlerTest` ✅ + +- Source: `src/test/java-excluded/org/jooby/internal/jetty/JettyHandlerTest.java` +- Target path: `src/test/java/org/jooby/internal/jetty/JettyHandlerTest.java` +- Restored as a current-behavior Jetty 10 test in `src/test/java/org/jooby/internal/jetty/JettyHandlerTest.java`. +- Re-review target: `src/main/java/org/jooby/internal/jetty/JettyHandler.java`. + +## 15. Rewrite `JettyServerTest` ✅ + +- Source: `src/test/java-excluded/org/jooby/internal/jetty/JettyServerTest.java` +- Target path: `src/test/java/org/jooby/internal/jetty/JettyServerTest.java` +- Restored as a direct Jetty 10 wiring test in `src/test/java/org/jooby/internal/jetty/JettyServerTest.java`. +- Re-review target: `src/main/java/org/jooby/internal/jetty/JettyServer.java`. + +## 16. Restore `SseFeature` ✅ + +- Source: `src/test/java-excluded/SseFeature.java` +- Target path: `src/test/java/org/jooby/test/SseFeature.java` +- Restored as a JDK 11 `HttpClient`-based SSE utility in `src/test/java/org/jooby/test/SseFeature.java`. +- Re-review target: current test utilities under `src/test/java/org/jooby/test/`, especially `Client.java`. + +- Current final Phase 1 test state: `124` Java files in `src/test/java`, `108` runnable test classes, `923` tests, and `src/test/java-excluded/` empty. + +--- + +# Phase 2 — JDK & Guice Foundation Upgrades + +> These steps prepare the foundation for the Jakarta namespace migration. +> They do NOT change any `javax.*` imports — only upgrade the JDK and Guice versions. + +## 1. Upgrade to JDK 17 (Prerequisite for Jakarta) + +- Update `killbill-oss-parent` to set `project.build.targetJdk=17` and `maven.compiler.release=17`. +- Fix any JDK 17 deprecation warnings or removed APIs across all modules (including `killbill-jooby`). +- Verify all dependencies are compatible with JDK 17 (check for illegal reflective access warnings). +- Run the full test suite under JDK 17 to establish a green baseline. +- Update CI pipelines to build and test with JDK 17. + +## 2. Upgrade to Guice 6.0 (Bridge Version) + +- Guice 6.0 supports **both** `javax.inject` and `jakarta.inject` simultaneously, making it the ideal bridge. +- Update `killbill-oss-parent` dependency management to use `com.google.inject:guice:6.0.0`. +- Update `com.google.inject.extensions:guice-servlet` to `6.0.0`. +- Fix any API incompatibilities in `killbill-jooby` caused by the Guice upgrade (Jooby 1.6.9 was written against Guice 4.2; Kill Bill currently uses 5.1.0). +- Run the full test suite to confirm Guice 6.0 is a drop-in replacement for 5.x. +- Note: Guice 6.0's `servlet` and `persist` extensions still use `javax.*` — this is expected and fine. + +--- + +# Phase 3 — Jakarta Namespace Migration (javax → jakarta) + +> Migrate all `javax.*` imports to `jakarta.*` one namespace at a time. +> Guice 6.0 (from Phase 2) supports both namespaces, so modules can be migrated incrementally. + +## 1. javax.inject → jakarta.inject + +- Replace `javax.inject:javax.inject` with `jakarta.inject:jakarta.inject-api:2.0.1` in all POMs. +- In all Java source files, replace `import javax.inject.*` with `import jakarta.inject.*`. + - Affected modules: `skeleton` (5 files), `queue` (4 files), `jdbi` (2 files), `metrics` (1 file), and `killbill-jooby` (forked Jooby source). + - Affected annotations: `@Inject`, `@Named`, `@Singleton`, `@Provider`. +- Verify Guice 6.0 resolves `jakarta.inject` annotations correctly alongside any remaining `javax.inject` from transitive deps. +- Run the full test suite to confirm no injection failures. + +## 2. javax.servlet → jakarta.servlet + +- Upgrade `jakarta.servlet:jakarta.servlet-api` from `4.x` (transitional) to `6.0.0` (true Jakarta namespace). +- In all Java source files, replace `import javax.servlet.*` with `import jakarta.servlet.*`. + - Affected modules: `skeleton` (8 files), `metrics` (4 files), and `killbill-jooby` (forked Jooby servlet/jetty source under `org.jooby.servlet`/`org.jooby.jetty` packages). + - Affected classes: `HttpServlet`, `ServletContext`, `Filter`, `FilterChain`, `HttpServletRequest`, `HttpServletResponse`, etc. +- Update `GuiceServletContextListener` and `JULServletContextListener` to use `jakarta.servlet` equivalents. +- Note: `guice-servlet` 6.0 still uses `javax.servlet` — this creates a temporary incompatibility. + - Workaround: keep `guice-servlet` on 6.0 and defer `skeleton`/`metrics` servlet migration until Phase 3 / Step 4 (Guice 7.0). + - Alternative: migrate `skeleton` servlet code and `guice-servlet` together in Phase 3 / Step 4. + +## 3. javax.ws.rs → jakarta.ws.rs + Jersey 3.x (skeleton only) + +- Upgrade `jakarta.ws.rs:jakarta.ws.rs-api` from `2.x` (transitional) to `3.1.0` (true Jakarta namespace). +- In all Java source files, replace `import javax.ws.rs.*` with `import jakarta.ws.rs.*`. + - Affected module: `skeleton` only (6 files). + - Affected annotations: `@Path`, `@GET`, `@POST`, `@Produces`, `@Consumes`, `@QueryParam`, `@PathParam`, etc. +- Upgrade Jersey from **2.39.1 to 3.x** (Jersey 3.x uses `jakarta.ws.rs`). + - Update all `org.glassfish.jersey.*` dependencies to their 3.x equivalents. + - Update `org.glassfish.hk2:guice-bridge` and `hk2-api` to Jakarta-compatible versions. +- Run skeleton tests (`TestJerseyBaseServerModule`) to verify Jersey 3.x + Jakarta JAX-RS works. + +## 4. Upgrade to Guice 7.0.0 (Final Jakarta) + +- Guice 7.0.0 supports **only** `jakarta.inject`, `jakarta.servlet`, `jakarta.persistence` — no `javax.*` at all. +- This step MUST come after Phase 2 / Step 2 and the earlier Jakarta migration steps in this phase are complete. +- Update `killbill-oss-parent` to use `com.google.inject:guice:7.0.0`. +- Update `com.google.inject.extensions:guice-servlet` to `7.0.0` (now uses `jakarta.servlet`). + - This resolves the `guice-servlet` / `jakarta.servlet` incompatibility noted above. +- Verify no transitive dependency still pulls in `javax.inject` or `javax.servlet` (use `mvn dependency:tree`). +- Run the full test suite to confirm everything works with Guice 7.0.0. + +## 5. javax.xml.bind → jakarta.xml.bind (JAXB) + +- Upgrade `jakarta.xml.bind:jakarta.xml.bind-api` from transitional to `4.0.0` (true Jakarta namespace). +- In all Java source files, replace `import javax.xml.bind.*` with `import jakarta.xml.bind.*`. + - Affected modules: `automaton` (7 files) and `xmlloader` (7 files). + - Affected classes: `JAXBContext`, `Marshaller`, `Unmarshaller`, `@XmlRootElement`, `@XmlElement`, etc. +- Update JAXB runtime implementation (e.g., `org.glassfish.jaxb:jaxb-runtime`) to 4.x. +- Run automaton and xmlloader tests to verify XML serialization/deserialization still works. +- This step is independent of Guice and can be done in parallel with the earlier Jakarta migration work. + +--- + +# Phase 4 — Migrate killbill-jooby Fork to Jooby 3.x (Jakarta-native) + +> With the codebase fully on Jakarta (Phase 3), update the forked Jooby source from 1.6.9 +> to the 3.x codebase. This is a re-fork, not a library upgrade — replace the vendored source. + +## 1. Re-fork Jooby Source from 3.x + +- Jooby 3.x is a major rewrite — it uses `jakarta.*` namespace and has a different API surface. +- Download/clone the latest Jooby 3.x release tag from upstream. +- Inventory API differences between 1.6.9 and 3.x that affect downstream consumers. + - `Jooby.Module` → `Extension` interface. + - `Request`/`Response` → `Context` handler signature. + - `Env.onStart`/`onStop` → Jooby 3.x lifecycle callbacks. + - groupId changed from `org.jooby` to `io.jooby`. +- Replace the forked source files in `jooby/src/main/java/` with Jooby 3.x source (core + servlet + jetty + jackson, merged into one flat module). +- Replace upstream test files in `jooby/src/test/java/` with Jooby 3.x tests. +- Update `jooby/pom.xml` dependencies to match Jooby 3.x requirements. +- Re-apply any Kill Bill-specific modifications that were made to the 1.6.9 fork (if any). +- Update `README.md` with the new upstream commit SHA and version. +- Run `mvn clean install` to verify compilation and tests pass. + +--- + +# Phase 5 — JDK Target Upgrades + +> With all dependencies on Jakarta and modern versions, upgrade the JDK target. + +## 1. Upgrade to JDK 21 + +- Update `killbill-oss-parent` to set `maven.compiler.release=21`. +- Fix any JDK 21 deprecation warnings or removed APIs (e.g., `SecurityManager` removal, finalization deprecation). +- Verify all dependencies (Guice 7, Jersey 3, forked Jooby 3.x, etc.) are compatible with JDK 21. +- Run the full test suite under JDK 21. +- Consider adopting JDK 21 features incrementally (virtual threads, pattern matching, sealed classes) in later tasks. + +## 2. Upgrade to JDK 25 (Future) + +- Update `maven.compiler.release=25` once JDK 25 is GA and dependencies support it. +- Address any further API removals or deprecations introduced between JDK 21 and 25. +- Verify all third-party dependencies have JDK 25-compatible releases. +- Run full test suite and integration tests under JDK 25. + +--- + +# Cross-Cutting Concerns (All Phases) + +## 1. Process & Verification Guidelines + +- Each section (step) must be a **separate branch and PR** — never bundle multiple steps. +- After each step, run `mvn dependency:tree -Dverbose` to check for split-package or duplicate-namespace conflicts. +- After each step, verify downstream projects (`killbill`, `killbill-plugin-api`, etc.) still compile against the updated `killbill-commons`. +- Maintain a compatibility matrix documenting which JDK/Guice/Jakarta versions each `killbill-commons` release supports. +- Consider using OpenRewrite recipes (`org.openrewrite.java.migrate.jakarta`) to automate bulk `javax → jakarta` refactoring. +- Keep `jsr305` (`@Nullable`, `@Nonnull`) as-is — these are not part of the Jakarta EE migration. +- When migrating `killbill-skeleton`, decide whether to keep it alongside `killbill-jooby` or deprecate it. + - If deprecated, mark with `@Deprecated` and update downstream consumers before removal.