@@ -95,4 +95,178 @@ public function testExpandedAttributesNoParameter()
9595 $ this ->assertEmpty (array_filter ($ openapi ['paths ' ][$ endpoint ['path ' ]]['get ' ]['parameters ' ], static fn ($ v ) => $ v ['name ' ] === $ endpoint ['placeholder ' ]));
9696 }
9797 }
98+
99+ private function diffSchemaPaths ($ snapshot , $ schema )
100+ {
101+ // Compare OpenAPI route paths and return differences
102+ $ differences = [];
103+ // ignore "/Assets/Custom' paths
104+ $ snapshot_paths = array_filter ($ snapshot ['paths ' ] ?? [], static fn ($ p ) => !str_starts_with ($ p , '/Assets/Custom ' ), ARRAY_FILTER_USE_KEY );
105+ $ schema_paths = array_filter ($ schema ['paths ' ] ?? [], static fn ($ p ) => !str_starts_with ($ p , '/Assets/Custom ' ), ARRAY_FILTER_USE_KEY );
106+ $ common_paths = array_intersect (array_keys ($ snapshot_paths ), array_keys ($ schema_paths ));
107+
108+ if (count ($ common_paths ) < count ($ snapshot_paths ) || count ($ common_paths ) < count ($ schema_paths )) {
109+ $ missing_in_schema = array_diff (array_keys ($ snapshot_paths ), array_keys ($ schema_paths ));
110+ $ missing_in_snapshot = array_diff (array_keys ($ schema_paths ), array_keys ($ snapshot_paths ));
111+ if (!empty ($ missing_in_schema )) {
112+ $ differences [] = 'Paths missing in schema: ' . implode (', ' , $ missing_in_schema );
113+ }
114+ if (!empty ($ missing_in_snapshot )) {
115+ $ differences [] = 'Paths missing in snapshot: ' . implode (', ' , $ missing_in_snapshot );
116+ }
117+ }
118+
119+ foreach ($ common_paths as $ path ) {
120+ foreach (['get ' , 'post ' , 'put ' , 'delete ' , 'patch ' ] as $ method ) {
121+ $ snapshot_method = $ snapshot_paths [$ path ][$ method ] ?? null ;
122+ $ schema_method = $ schema_paths [$ path ][$ method ] ?? null ;
123+ if ($ snapshot_method === null && $ schema_method === null ) {
124+ continue ;
125+ } elseif ($ snapshot_method === null ) {
126+ $ differences [] = "Method ' $ method' for path ' $ path' is missing in the snapshot " ;
127+ continue ;
128+ } elseif ($ schema_method === null ) {
129+ $ differences [] = "Method ' $ method' for path ' $ path' is missing in the schema " ;
130+ continue ;
131+ }
132+ unset($ snapshot_method ['description ' ], $ schema_method ['description ' ], $ snapshot_method ['tags ' ], $ schema_method ['tags ' ]);
133+ if ($ snapshot_method !== $ schema_method ) {
134+ $ differences [] = "Method ' $ method' for path ' $ path' differs between snapshot and schema " ;
135+ }
136+ }
137+ }
138+ return $ differences ;
139+ }
140+
141+ private function diffSchemaProperties ($ snapshot_props , $ schema_props , $ parent_path = '' )
142+ {
143+ $ differences = [];
144+ $ common_props = array_intersect (array_keys ($ snapshot_props ), array_keys ($ schema_props ));
145+
146+ if (count ($ common_props ) < count ($ snapshot_props ) || count ($ common_props ) < count ($ schema_props )) {
147+ $ missing_in_schema = array_diff (array_keys ($ snapshot_props ), array_keys ($ schema_props ));
148+ $ missing_in_snapshot = array_diff (array_keys ($ schema_props ), array_keys ($ snapshot_props ));
149+ if (!empty ($ missing_in_schema )) {
150+ $ differences [] = 'Properties missing in schema at ' . $ parent_path . ': ' . implode (', ' , $ missing_in_schema );
151+ }
152+ if (!empty ($ missing_in_snapshot )) {
153+ $ differences [] = 'Properties missing in snapshot at ' . $ parent_path . ': ' . implode (', ' , $ missing_in_snapshot );
154+ }
155+ }
156+
157+ foreach ($ common_props as $ prop_name ) {
158+ $ snapshot_prop = $ snapshot_props [$ prop_name ];
159+ $ schema_prop = $ schema_props [$ prop_name ];
160+ unset($ snapshot_prop ['description ' ], $ schema_prop ['description ' ]);
161+
162+ // Recursively compare nested properties
163+ if (isset ($ snapshot_prop ['properties ' ], $ schema_prop ['properties ' ])) {
164+ $ nested_diffs = $ this ->diffSchemaProperties (
165+ $ snapshot_prop ['properties ' ],
166+ $ schema_prop ['properties ' ],
167+ $ parent_path . $ prop_name . '. '
168+ );
169+ $ differences = array_merge ($ differences , $ nested_diffs );
170+ } elseif (isset ($ snapshot_prop ['items ' ]['properties ' ], $ schema_prop ['items ' ]['properties ' ])) {
171+ $ nested_diffs = $ this ->diffSchemaProperties (
172+ $ snapshot_prop ['items ' ]['properties ' ],
173+ $ schema_prop ['items ' ]['properties ' ],
174+ $ parent_path . $ prop_name . '[] ' . '. '
175+ );
176+ $ differences = array_merge ($ differences , $ nested_diffs );
177+ } elseif ($ snapshot_prop != $ schema_prop ) {
178+ $ differences [] = "Property ' $ parent_path$ prop_name' differs between snapshot and schema " ;
179+ }
180+ }
181+
182+ return $ differences ;
183+ }
184+
185+ private function diffComponentSchemas ($ snapshot , $ schema )
186+ {
187+ // Compare OpenAPI component schemas and return differences
188+ $ differences = [];
189+ // Ignore custom assets
190+ $ snapshot_schemas = array_filter ($ snapshot ['components ' ]['schemas ' ] ?? [], static fn ($ k ) => !str_starts_with ($ k , 'Custom ' ), ARRAY_FILTER_USE_KEY );
191+ $ schema_schemas = array_filter ($ schema ['components ' ]['schemas ' ] ?? [], static fn ($ k ) => !str_starts_with ($ k , 'Custom ' ), ARRAY_FILTER_USE_KEY );
192+ $ common_schemas = array_intersect (array_keys ($ snapshot_schemas ), array_keys ($ schema_schemas ));
193+
194+ if (count ($ common_schemas ) < count ($ snapshot_schemas ) || count ($ common_schemas ) < count ($ schema_schemas )) {
195+ $ missing_in_schema = array_diff (array_keys ($ snapshot_schemas ), array_keys ($ schema_schemas ));
196+ $ missing_in_snapshot = array_diff (array_keys ($ schema_schemas ), array_keys ($ snapshot_schemas ));
197+ if (!empty ($ missing_in_schema )) {
198+ $ differences [] = 'Component schemas missing in schema: ' . implode (', ' , $ missing_in_schema );
199+ }
200+ if (!empty ($ missing_in_snapshot )) {
201+ $ differences [] = 'Component schemas missing in snapshot: ' . implode (', ' , $ missing_in_snapshot );
202+ }
203+ }
204+
205+ foreach ($ common_schemas as $ schema_name ) {
206+ $ snapshot_schema = $ snapshot_schemas [$ schema_name ];
207+ $ schema_schema = $ schema_schemas [$ schema_name ];
208+ unset($ snapshot_schema ['description ' ], $ schema_schema ['description ' ]);
209+
210+ // Compare properties recursively
211+ if (isset ($ snapshot_schema ['properties ' ], $ schema_schema ['properties ' ])) {
212+ $ prop_diffs = $ this ->diffSchemaProperties (
213+ $ snapshot_schema ['properties ' ],
214+ $ schema_schema ['properties ' ],
215+ $ schema_name . '. '
216+ );
217+ $ differences = array_merge ($ differences , $ prop_diffs );
218+ }
219+ unset($ snapshot_schema ['properties ' ], $ schema_schema ['properties ' ]);
220+ if ($ snapshot_schema !== $ schema_schema ) {
221+ $ differences [] = "Component schema ' $ schema_name' differs between snapshot and schema " ;
222+ }
223+ }
224+ return $ differences ;
225+ }
226+
227+ private function assertSchemaMatchesSnapshot (array $ snapshot , array $ schema )
228+ {
229+ $ path_differences = $ this ->diffSchemaPaths ($ snapshot , $ schema );
230+ $ component_differences = $ this ->diffComponentSchemas ($ snapshot , $ schema );
231+
232+ if (!empty ($ path_differences ) || !empty ($ component_differences )) {
233+ $ version = $ schema ['info ' ]['version ' ];
234+ $ this ->fail ("Schema for v {$ version } does not match snapshot: \n" . implode ("\n" , $ path_differences + $ component_differences ));
235+ }
236+ }
237+
238+ /**
239+ * Ensure schemas do not change unexpectedly for API versions
240+ * @return void
241+ */
242+ public function testSchemaSnapshot ()
243+ {
244+ $ this ->login ();
245+
246+ $ snapshot_dir = __DIR__ . '/../../../../fixtures/hlapi/snapshots ' ;
247+ $ this ->assertDirectoryExists ($ snapshot_dir , "Snapshot directory does not exist: $ snapshot_dir " );
248+
249+ $ router = Router::getInstance ();
250+ $ api_versions = $ router ::getAPIVersions ();
251+ // Only care about the initial minor versions (2.0.0 and 2.1.0 but not 2.1.1, etc)
252+ $ initial_minor_versions = [];
253+ foreach ($ api_versions as $ version_info ) {
254+ if ((int ) $ version_info ['api_version ' ] === 1 ) {
255+ continue ;
256+ }
257+ $ version = $ version_info ['version ' ];
258+ if (preg_match ('/\d+\.\d+\.0$/ ' , $ version )) {
259+ $ initial_minor_versions [] = $ version ;
260+ }
261+ }
262+
263+ foreach ($ initial_minor_versions as $ version ) {
264+ $ openapi_generator = new OpenAPIGenerator ($ router , $ version );
265+ $ schema = $ openapi_generator ->getSchema ();
266+ $ snapshot_file = $ snapshot_dir . '/v ' . str_replace ('. ' , '_ ' , $ version ) . '.json ' ;
267+ $ this ->assertFileExists ($ snapshot_file , "Snapshot file does not exist for version $ version: $ snapshot_file " );
268+ $ expected_schema = json_decode (file_get_contents ($ snapshot_file ), true );
269+ $ this ->assertSchemaMatchesSnapshot ($ expected_schema , $ schema );
270+ }
271+ }
98272}
0 commit comments