1+ <?php namespace web \unittest \filters ;
2+
3+ use test \{Assert , Test , Values };
4+ use web \filters \{CORS , Invocation };
5+ use web \io \{TestInput , TestOutput };
6+ use web \{Filter , Request , Response };
7+
8+ class CORSTest {
9+ const ORIGIN = 'http://example.com ' ;
10+ const RESPONSE = ['Content-Type ' => 'text/plain ' , 'Content-Length ' => 9 ];
11+
12+ private function filter (CORS $ fixture , $ method , $ uri , $ headers = [], $ body = null ) {
13+ $ req = new Request (new TestInput ($ method , $ uri , $ headers , $ body ?? '' ));
14+ $ res = new Response (new TestOutput ());
15+ $ fixture ->filter ($ req , $ res , new Invocation (function ($ req , $ res ) {
16+ $ res ->send ('Completed ' , 'text/plain ' );
17+ }));
18+ return $ res ;
19+ }
20+
21+ /** Values for preflight test */
22+ private function preflights (): iterable {
23+ $ cors = (new CORS ()->origins (self ::ORIGIN ));
24+ yield [$ cors , []];
25+ yield [(clone $ cors )->origins (fn ($ origin ) => self ::ORIGIN === $ origin ? $ origin : null ), []];
26+ yield [(clone $ cors )->origins ('* ' ), ['Access-Control-Allow-Origin ' => '* ' ]];
27+
28+ // Methods
29+ yield [(clone $ cors )->methods (null ), []];
30+ yield [(clone $ cors )->methods ([]), []];
31+ yield [(clone $ cors )->methods ('GET, POST ' ), ['Access-Control-Allow-Methods ' => 'GET, POST ' ]];
32+ yield [(clone $ cors )->methods (['GET ' , 'POST ' ]), ['Access-Control-Allow-Methods ' => 'GET, POST ' ]];
33+
34+ // Headers
35+ yield [(clone $ cors )->headers (null ), []];
36+ yield [(clone $ cors )->headers ([]), []];
37+ yield [(clone $ cors )->headers ('X-Input ' ), ['Access-Control-Allow-Headers ' => 'X-Input ' ]];
38+ yield [(clone $ cors )->headers (['X-Input ' ]), ['Access-Control-Allow-Headers ' => 'X-Input ' ]];
39+
40+ // Age
41+ yield [(clone $ cors )->maxAge (null ), []];
42+ yield [(clone $ cors )->maxAge (0 ), []];
43+ yield [(clone $ cors )->maxAge (86400 ), ['Access-Control-Max-Age ' => '86400 ' ]];
44+
45+ // Expose
46+ yield [(clone $ cors )->expose (null ), []];
47+ yield [(clone $ cors )->expose ([]), []];
48+ yield [(clone $ cors )->expose ('X-Output ' ), ['Access-Control-Expose-Headers ' => 'X-Output ' ]];
49+ yield [(clone $ cors )->expose (['X-Output ' ]), ['Access-Control-Expose-Headers ' => 'X-Output ' ]];
50+
51+ // Credentials
52+ yield [(clone $ cors )->credentials (false ), []];
53+ yield [(clone $ cors )->credentials (true ), ['Access-Control-Allow-Credentials ' => 'true ' ]];
54+ }
55+
56+ /** Values for request test */
57+ private function requests (): iterable {
58+ $ cors = (new CORS ()->origins (self ::ORIGIN ));
59+ yield [$ cors , []];
60+
61+ // Only included in preflight
62+ yield [(clone $ cors )->methods (['GET ' , 'POST ' ]), []];
63+ yield [(clone $ cors )->headers (['X-Input ' ]), []];
64+ yield [(clone $ cors )->maxAge (86400 ), []];
65+
66+ // Included in all requests
67+ yield [(clone $ cors )->expose (['X-Output ' ]), ['Access-Control-Expose-Headers ' => 'X-Output ' ]];
68+ yield [(clone $ cors )->credentials (true ), ['Access-Control-Allow-Credentials ' => 'true ' ]];
69+ }
70+
71+ /** Values for allowing_origin_with_any_4_digit_port */
72+ private function origins (): iterable {
73+ yield [self ::ORIGIN , true ];
74+ yield [self ::ORIGIN .':3000 ' , true ];
75+
76+ // Not allowed
77+ yield [self ::ORIGIN .':443 ' , false ];
78+ yield [strtr (self ::ORIGIN , ['http: ' => 'https: ' ]), false ];
79+ yield ['http://localhost ' , false ];
80+ yield ['' , false ];
81+ }
82+
83+ #[Test]
84+ public function can_create () {
85+ new CORS ();
86+ }
87+
88+ #[Test]
89+ public function request_without_origin_receives_no_cors () {
90+ $ response = $ this ->filter (new CORS (), 'GET ' , '/ ' );
91+
92+ Assert::equals (200 , $ response ->status ());
93+ Assert::equals (self ::RESPONSE , $ response ->headers ());
94+ }
95+
96+ #[Test, Values(from: 'preflights ' )]
97+ public function preflight ($ fixture , $ expected ) {
98+ $ response = $ this ->filter ($ fixture , 'OPTIONS ' , '/ ' , [
99+ 'Origin ' => self ::ORIGIN ,
100+ 'Access-Control-Request-Method ' => 'GET ' ,
101+ 'Access-Control-Request-Headers ' => 'X-Input ' ,
102+ ]);
103+
104+ Assert::equals (204 , $ response ->status ());
105+ Assert::equals (
106+ $ expected + ['Vary ' => 'Origin ' , 'Access-Control-Allow-Origin ' => self ::ORIGIN ],
107+ $ response ->headers ()
108+ );
109+ }
110+
111+ #[Test, Values(from: 'requests ' )]
112+ public function request ($ fixture , $ expected ) {
113+ $ response = $ this ->filter ($ fixture , 'GET ' , '/ ' , ['Origin ' => self ::ORIGIN ]);
114+
115+ Assert::equals (200 , $ response ->status ());
116+ Assert::equals (
117+ $ expected + ['Vary ' => 'Origin ' , 'Access-Control-Allow-Origin ' => self ::ORIGIN , ...self ::RESPONSE ],
118+ $ response ->headers ()
119+ );
120+ }
121+
122+ #[Test, Values(from: 'origins ' )]
123+ public function allowing_origin_with_any_4_digit_port ($ origin , $ allow ) {
124+ $ fixture = (new CORS ())->origins (function ($ origin ) {
125+ return preg_match ('/^ ' .preg_quote (self ::ORIGIN , '/ ' ).'(:[0-9]{4})?$/ ' , $ origin ) ? $ origin : null ;
126+ });
127+ $ response = $ this ->filter ($ fixture , 'GET ' , '/ ' , ['Origin ' => $ origin ]);
128+
129+ Assert::equals (200 , $ response ->status ());
130+ Assert::equals (
131+ ($ allow ? ['Access-Control-Allow-Origin ' => $ origin ] : []) + ['Vary ' => 'Origin ' , ...self ::RESPONSE ],
132+ $ response ->headers ()
133+ );
134+ }
135+ }
0 commit comments