11package sprout .core .filter .cors ;
22
3- import org .junit .jupiter .api .BeforeEach ;
4- import org .junit .jupiter .api .DisplayName ;
5- import org .junit .jupiter .api .Nested ;
6- import org .junit .jupiter .api .Test ;
3+ import org .junit .jupiter .api .*;
74import org .mockito .InOrder ;
85import org .mockito .Mock ;
96import org .mockito .MockitoAnnotations ;
7+ import sprout .config .AppConfig ;
108import sprout .core .filter .FilterChain ;
119import sprout .mvc .http .HttpMethod ;
1210import sprout .mvc .http .HttpRequest ;
1311import sprout .mvc .http .HttpResponse ;
1412
1513import java .io .IOException ;
14+ import java .util .Map ;
1615
1716import static org .mockito .Mockito .*;
1817
@@ -23,61 +22,120 @@ class CorsFilterTest {
2322 @ Mock HttpRequest request ;
2423 @ Mock HttpResponse response ;
2524 @ Mock FilterChain chain ;
25+ @ Mock AppConfig appConfig ;
26+
27+ AutoCloseable mocks ;
2628
2729 @ BeforeEach
2830 void setUp () {
29- MockitoAnnotations .openMocks (this );
30- corsFilter = new CorsFilter ();
31+ mocks = MockitoAnnotations .openMocks (this );
32+ corsFilter = new CorsFilter (appConfig );
33+ when (appConfig .getStringProperty (eq ("cors.allow-origin" ), anyString ())).thenReturn ("*" );
34+ when (appConfig .getStringProperty (eq ("cors.allow-credentials" ), anyString ())).thenReturn ("false" );
35+ when (appConfig .getStringProperty (eq ("cors.allow-methods" ), anyString ())).thenReturn ("GET, POST, PUT, DELETE, OPTIONS" );
36+ when (appConfig .getStringProperty (eq ("cors.allow-headers" ), anyString ())).thenReturn ("Content-Type, Authorization" );
37+ when (appConfig .getStringProperty (eq ("cors.expose-headers" ), anyString ())).thenReturn ("" );
38+ when (appConfig .getStringProperty (eq ("cors.max-age" ), anyString ())).thenReturn ("3600" );
39+ }
40+
41+ @ AfterEach
42+ void tearDown () throws Exception {
43+ if (mocks != null ) mocks .close ();
3144 }
3245
33- /* ---------- OPTIONS 사전 요청 ---------- */
46+ // --- 작은 헬퍼: request.getHeaders()를 항상 목으로 주입 ---
47+ @ SuppressWarnings ("unchecked" )
48+ private Map <String , Object > headers () {
49+ Map <String , Object > h = (Map <String , Object >) mock (Map .class );
50+ when (request .getHeaders ()).thenReturn (h );
51+ return h ;
52+ }
3453
3554 @ Nested
36- @ DisplayName ("OPTIONS pre‑flight 요청" )
37- class Options {
55+ @ DisplayName ("Origin 헤더가 없는 요청" )
56+ class NoOrigin {
57+ @ Test
58+ void skipsCorsAndForwardsChain () throws IOException {
59+ Map <String , Object > h = headers ();
60+ when (h .get ("Origin" )).thenReturn (null );
61+ when (request .getMethod ()).thenReturn (HttpMethod .GET );
3862
63+ corsFilter .doFilter (request , response , chain );
64+
65+ verify (chain ).doFilter (request , response );
66+ verify (response , never ()).addHeader (eq ("Access-Control-Allow-Origin" ), anyString ());
67+ }
68+ }
69+
70+ @ Nested
71+ @ DisplayName ("OPTIONS pre-flight 요청" )
72+ class Options {
3973 @ Test
40- @ DisplayName ("Max‑Age 헤더를 추가하고 체인을 진행하지 않는다" )
41- void handlesOptionsWithoutCallingChain () throws IOException {
74+ @ DisplayName ("Max-Age와 Allow-*를 설정하고 단락한다" )
75+ void handlesOptionsPreflight () throws IOException {
76+ Map <String , Object > h = headers ();
77+ when (h .get ("Origin" )).thenReturn ("https://app.example.com" );
78+ when (h .get ("Access-Control-Request-Method" )).thenReturn ("PATCH" );
79+ when (h .get ("Access-Control-Request-Headers" )).thenReturn ("X-Trace-Id, Authorization" );
4280 when (request .getMethod ()).thenReturn (HttpMethod .OPTIONS );
4381
4482 corsFilter .doFilter (request , response , chain );
4583
46- // 공통 CORS 헤더 + Max‑Age 검증
47- verify (response ).addHeader ("Access-Control-Allow-Origin" , "*" );
48- verify (response ).addHeader ("Access-Control-Allow-Methods" ,"GET, POST, PUT, DELETE, OPTIONS" );
49- verify (response ).addHeader ("Access-Control-Allow-Headers" ,"Content-Type, Authorization" );
50- verify (response ).addHeader ("Access-Control-Max-Age" , "3600" );
51-
52- // 체인이 호출되지 않아야 한다
84+ verify (response ).addHeader ("Vary" ,"Origin" );
85+ verify (response ).addHeader ("Vary" ,"Access-Control-Request-Method" );
86+ verify (response ).addHeader ("Vary" ,"Access-Control-Request-Headers" );
87+ verify (response ).addHeader ("Access-Control-Allow-Origin" ,"*" );
88+ verify (response ).addHeader ("Access-Control-Allow-Methods" ,"PATCH" );
89+ verify (response ).addHeader ("Access-Control-Allow-Headers" ,"X-Trace-Id, Authorization" );
90+ verify (response ).addHeader ("Access-Control-Max-Age" ,"3600" );
91+ verify (response ).addHeader ("Content-Length" ,"0" );
5392 verifyNoInteractions (chain );
5493 }
5594 }
5695
57- /* ---------- 일반(비‑OPTIONS) 요청 ---------- */
58-
5996 @ Nested
6097 @ DisplayName ("일반 HTTP 요청" )
6198 class NonOptions {
62-
6399 @ Test
64- @ DisplayName ("CORS 헤더를 추가한 뒤 체인을 계속 진행한다" )
100+ @ DisplayName ("CORS 헤더를 추가하고 체인을 계속 진행한다" )
65101 void addsHeadersAndForwards () throws IOException {
102+ Map <String , Object > h = headers ();
103+ when (h .get ("Origin" )).thenReturn ("https://app.example.com" );
66104 when (request .getMethod ()).thenReturn (HttpMethod .GET );
67105
68106 corsFilter .doFilter (request , response , chain );
69107
70- // 헤더 추가 순서까지 확인 (선택)
71108 InOrder in = inOrder (response , chain );
109+ in .verify (response ).addHeader ("Vary" ,"Origin" );
110+ in .verify (response ).addHeader ("Vary" ,"Access-Control-Request-Method" );
111+ in .verify (response ).addHeader ("Vary" ,"Access-Control-Request-Headers" );
72112 in .verify (response ).addHeader ("Access-Control-Allow-Origin" ,"*" );
73113 in .verify (response ).addHeader ("Access-Control-Allow-Methods" ,"GET, POST, PUT, DELETE, OPTIONS" );
74114 in .verify (response ).addHeader ("Access-Control-Allow-Headers" ,"Content-Type, Authorization" );
115+ in .verify (chain ).doFilter (request , response );
75116
76- // Max‑Age 헤더는 없음
77117 verify (response , never ()).addHeader (eq ("Access-Control-Max-Age" ), anyString ());
118+ }
119+ }
78120
79- // 체인 진행
80- in .verify (chain ).doFilter (request , response );
121+ @ Nested
122+ @ DisplayName ("Credentials 허용 시" )
123+ class Credentials {
124+ @ Test
125+ @ DisplayName ("와일드카드 대신 요청 Origin을 반사한다" )
126+ void reflectsOriginWhenCredentialsTrue () throws IOException {
127+ when (appConfig .getStringProperty ("cors.allow-credentials" ,"false" )).thenReturn ("true" );
128+ when (appConfig .getStringProperty ("cors.allow-origin" ,"*" )).thenReturn ("*" );
129+
130+ Map <String , Object > h = headers ();
131+ when (h .get ("Origin" )).thenReturn ("https://secure.example.com" );
132+ when (request .getMethod ()).thenReturn (HttpMethod .GET );
133+
134+ corsFilter .doFilter (request , response , chain );
135+
136+ verify (response ).addHeader ("Access-Control-Allow-Origin" ,"https://secure.example.com" );
137+ verify (response ).addHeader ("Access-Control-Allow-Credentials" ,"true" );
138+ verify (chain ).doFilter (request , response );
81139 }
82140 }
83141}
0 commit comments