Skip to content

Commit a5321d2

Browse files
Test/architecture test (#121)
* Add ArchUnit dependency * Create ArchitectureTest * Create initial ArchRule * Update connectionHandlerAccessRule test * Add class description * Create pipelineAccessRule * Create filterChainRule * Create routerRule * Update filterChainRule and pipelineAccessRule * Create pluginRule and update docs * Update README.md Architecture Overview * Update pom.xml * refactor: migrate ArchUnit tests from @archtest to explicit ClassFileImporter setup * Update pipelineAccessRule to include DefaultConnectionHandlerFactory * Update pipelineAccessRule to include new Bootstrap class * Update pipelineAccessRule. Remove TODO decouple Server from Pipeline * Create httpResponseWriterAccessRule
1 parent ad74e89 commit a5321d2

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Pipeline
4747
4848
FilterChain
4949
50+
Router
51+
5052
Plugin
5153
5254
HttpResponseWriter

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
<version>${mockito.version}</version>
4444
<scope>test</scope>
4545
</dependency>
46+
<dependency>
47+
<groupId>com.tngtech.archunit</groupId>
48+
<artifactId>archunit</artifactId>
49+
<version>1.4.1</version>
50+
<scope>test</scope>
51+
</dependency>
4652
<dependency>
4753
<groupId>org.yaml</groupId>
4854
<artifactId>snakeyaml</artifactId>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package org.juv25d;
2+
3+
import com.tngtech.archunit.core.domain.JavaClasses;
4+
import com.tngtech.archunit.core.importer.ClassFileImporter;
5+
import com.tngtech.archunit.core.importer.ImportOption;
6+
7+
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
8+
import org.junit.jupiter.api.BeforeAll;
9+
import org.junit.jupiter.api.Test;
10+
11+
12+
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
13+
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleName;
14+
15+
/**
16+
* This system follows a specific lifecycle where an HTTP request is processed through specialized layers.
17+
* To maintain a clean architecture, the core principle is that lower layers must never depend on higher layers.
18+
* While the flow is not strictly linear—for instance, filters must call the chain to proceed—these rules ensure that
19+
* dependencies only move in authorized directions and that components do not "skip" steps unnecessarily
20+
*
21+
* The request lifecycle is designed to follow this strict flow: (Runtime Flow)
22+
*
23+
* Client
24+
* ↓
25+
* ServerSocket
26+
* ↓
27+
* ConnectionHandler (Virtual Thread)
28+
* ↓
29+
* Pipeline
30+
* ↓
31+
* FilterChain
32+
* ↓
33+
* Router
34+
* ↓
35+
* Plugin
36+
* ↓
37+
* HttpResponseWriter
38+
* ↓
39+
* Client
40+
*
41+
* Note: This describes the runtime execution flow, not direct code dependencies.
42+
* Dependencies are allowed for bootstrapping and controlled object creation,
43+
* but must never violate the downward lifecycle direction.
44+
*/
45+
46+
public class ArchitectureTest {
47+
48+
private static JavaClasses importedClasses;
49+
50+
51+
@BeforeAll
52+
static void setup() {
53+
importedClasses = new ClassFileImporter()
54+
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
55+
.importPackages("org.juv25d");
56+
}
57+
/**
58+
* This rule ensures that only the Server and its associated factories can initiate a ConnectionHandler.
59+
* This prevents other parts of the application from accidentally manipulating direct client connections.
60+
*/
61+
@Test
62+
void connectionHandlerAccessRule () {
63+
ArchRuleDefinition.classes()
64+
.that().haveSimpleName("ConnectionHandler")
65+
.should().onlyBeAccessed().byClassesThat(
66+
simpleName("Server")
67+
.or(simpleName("ConnectionHandler"))
68+
.or(simpleName("DefaultConnectionHandlerFactory"))
69+
.or(simpleName("ConnectionHandlerFactory")))
70+
.as("ConnectionHandler access rule")
71+
.because("ConnectionHandler should only be accessed by server, connectionhandler or its factories")
72+
.check(importedClasses);
73+
}
74+
75+
/**
76+
* Only the network layer (ConnectionHandler) or the application's startup class (App) may interact with the Pipeline.
77+
* This guarantees that the execution chain remains intact and is not modified during an active request.
78+
*/
79+
@Test
80+
void pipelineAccessRule () {
81+
ArchRuleDefinition.classes()
82+
.that().haveSimpleName("Pipeline")
83+
.should().onlyBeAccessed().byClassesThat(
84+
simpleName("ConnectionHandler")
85+
.or(simpleName("ConnectionHandlerFactory"))
86+
.or(simpleName("DefaultConnectionHandlerFactory"))
87+
.or(simpleName("Pipeline"))
88+
.or(simpleName("App")) // App handles bootstrapping and wiring of the Pipeline during startup. This should stay.
89+
.or(simpleName("Bootstrap")))
90+
.as("Pipeline access rule")
91+
.because("Pipeline should only be accessed by ConnectionHandler, App, Bootstrap during setup")
92+
.check(importedClasses);
93+
}
94+
95+
96+
/**
97+
* The FilterChain is created by the Pipeline and triggered by the ConnectionHandler.
98+
* This rule also allows individual filters to access the chain.
99+
*/
100+
@Test
101+
void filterChainRule () {
102+
ArchRuleDefinition.classes()
103+
.that().haveSimpleName("FilterChain")
104+
.should().onlyBeAccessed().byClassesThat(
105+
simpleName("Pipeline")
106+
.or(simpleName("FilterChain"))
107+
.or(simpleName("FilterChainImpl"))
108+
.or(resideInAPackage("..filter.."))
109+
.or(simpleName("ConnectionHandler"))) //This needs to be accessed because ConnectionHandler creates doFilter()
110+
.as("FilterChain access rule")
111+
.because("FilterChain should only be accessed by Pipeline, ConnectionHandler")
112+
.check(importedClasses);
113+
}
114+
115+
116+
/**
117+
* The Router should only be accessed by the FilterChain to determine which plugin to execute,
118+
* or by App and Pipeline during the system's bootstrapping phase.
119+
*/
120+
@Test
121+
void routerRule () {
122+
ArchRuleDefinition.classes()
123+
.that().haveSimpleName("Router")
124+
.should().onlyBeAccessed().byClassesThat(
125+
simpleName("FilterChain")
126+
.or(simpleName("FilterChainImpl"))
127+
.or(simpleName("Router"))
128+
.or(simpleName("Pipeline")) //Pipeline injects router
129+
.or(simpleName("App"))) //App Creates router
130+
.as("Router access rule")
131+
.because("Router should only be accessed by FilterChain, Pipeline, App")
132+
.check(importedClasses);
133+
}
134+
135+
136+
/**
137+
* Plugins must only be instantiated by App at startup and subsequently called by the router or
138+
* the execution chain (FilterChainImpl).
139+
*/
140+
@Test
141+
void pluginRule () {
142+
ArchRuleDefinition.classes()
143+
.that().resideInAPackage("..plugin..")
144+
.should().onlyBeAccessed().byClassesThat(
145+
resideInAPackage("..router..")
146+
.or(resideInAPackage("..plugin.."))
147+
.or(simpleName("App")) //App creates plugin
148+
.or(simpleName("FilterChainImpl"))) //FilterChainImpl calls the plugin after the router has decided which one to run.
149+
.as("Plugin access rule")
150+
.because("Plugins should only be managed by the Router, App, FilterChainImpl")
151+
.check(importedClasses);
152+
}
153+
154+
/**
155+
* The HttpResponseWriter is the final step in the request lifecycle, responsible for delivering the response to the client.
156+
* This rule ensures that only the ConnectionHandler accesses it, guaranteeing controlled delivery and architectural integrity.
157+
*/
158+
@Test
159+
void httpResponseWriterAccessRule() {
160+
ArchRuleDefinition.classes()
161+
.that().haveSimpleName("HttpResponseWriter")
162+
.should().onlyBeAccessed().byClassesThat(
163+
simpleName("ConnectionHandler")
164+
.or(simpleName("HttpResponseWriter")))
165+
.as("HttpResponseWriter access rule")
166+
.because("HttpResponseWriter is the final step in the lifecycle and should" +
167+
"only be used by ConnectionHandler to ensure controlled delivery")
168+
.check(importedClasses);
169+
}
170+
}
171+
172+

0 commit comments

Comments
 (0)