@@ -126,7 +126,9 @@ final class GameHackingOrgLookupTests: XCTestCase {
126126 let json = #"[{"name":"Infinite Lives","code":"DEADBEEF00000001","category":"General"}]"#
127127 ProxyCannedProtocol . cannedJSON = Data ( json. utf8)
128128 ProxyCannedProtocol . statusCode = 200
129+ ProxyCannedProtocol . cannedHeaders = [ " X-Proxy-Status " : " ok " ]
129130 ProxyCannedProtocol . lastRequest = nil
131+ defer { ProxyCannedProtocol . cannedHeaders = [ : ] }
130132
131133 Defaults [ . useCheatProxy] = true
132134 Defaults [ . cheatProxyURL] = " https://test.proxy.pvemu.invalid "
@@ -151,14 +153,27 @@ final class GameHackingOrgLookupTests: XCTestCase {
151153 XCTAssertTrue ( entries. first? . isOnlineResult ?? false )
152154 }
153155
154- func testSearchCheats_proxyReturnsEmpty_fallsThrough( ) async {
156+ func testSearchCheats_proxyReturnsEmpty_noFallback( ) async {
157+ // Proxy returns [] with X-Proxy-Status: ok — meaning "upstream confirmed no cheats".
158+ // searchCheats should trust this and NOT fall back to direct scraping.
159+ // DirectScrapeBlockerProtocol is registered to ensure no gamehacking.org request is made.
155160 URLProtocol . registerClass ( ProxyCannedProtocol . self)
156- defer { URLProtocol . unregisterClass ( ProxyCannedProtocol . self) }
161+ URLProtocol . registerClass ( DirectScrapeBlockerProtocol . self)
162+ defer {
163+ URLProtocol . unregisterClass ( ProxyCannedProtocol . self)
164+ URLProtocol . unregisterClass ( DirectScrapeBlockerProtocol . self)
165+ }
157166
158- // Proxy returns empty array — direct scraping also yields nothing (no network in CI)
159167 ProxyCannedProtocol . cannedJSON = Data ( " [] " . utf8)
160168 ProxyCannedProtocol . statusCode = 200
169+ // X-Proxy-Status: ok tells the client the proxy successfully ran and found nothing
170+ ProxyCannedProtocol . cannedHeaders = [ " X-Proxy-Status " : " ok " ]
161171 ProxyCannedProtocol . lastRequest = nil
172+ DirectScrapeBlockerProtocol . requestCount = 0
173+ defer {
174+ ProxyCannedProtocol . cannedHeaders = [ : ]
175+ DirectScrapeBlockerProtocol . requestCount = 0
176+ }
162177
163178 Defaults [ . useCheatProxy] = true
164179 Defaults [ . cheatProxyURL] = " https://test.proxy.pvemu.invalid "
@@ -167,20 +182,31 @@ final class GameHackingOrgLookupTests: XCTestCase {
167182 Defaults . reset ( . cheatProxyURL)
168183 }
169184
170- let title = " ProxyEmptyFallback_ \( UUID ( ) . uuidString) "
185+ let title = " ProxyEmptyNoFallback_ \( UUID ( ) . uuidString) "
171186 let entries = await GameHackingOrgLookup . shared. searchCheats ( title: title, systemSlug: nil )
172- // Proxy was contacted but returned empty; direct scraping also fails offline — result is empty
187+
188+ // Proxy was contacted and returned empty with ok status — no direct scrape should happen
173189 XCTAssertTrue ( entries. isEmpty)
174190 let intercepted = ProxyCannedProtocol . lastRequest? . url? . absoluteString ?? " "
175- XCTAssertTrue ( intercepted. contains ( " /cheats " ) , " Proxy should still have been contacted even when empty " )
191+ XCTAssertTrue ( intercepted. contains ( " /cheats " ) , " Proxy should have been contacted " )
192+ XCTAssertEqual ( DirectScrapeBlockerProtocol . requestCount, 0 , " Direct scrape should NOT occur when proxy confirms no cheats " )
176193 }
177194
178195 func testSearchCheats_proxyDisabled_doesNotContactProxy( ) async {
196+ // Proxy is disabled — only the direct scrape path runs.
197+ // DirectScrapeBlockerProtocol intercepts gamehacking.org requests so no real
198+ // network call is made; it returns empty HTML so the scrape yields no results.
179199 URLProtocol . registerClass ( ProxyCannedProtocol . self)
180- defer { URLProtocol . unregisterClass ( ProxyCannedProtocol . self) }
200+ URLProtocol . registerClass ( DirectScrapeBlockerProtocol . self)
201+ defer {
202+ URLProtocol . unregisterClass ( ProxyCannedProtocol . self)
203+ URLProtocol . unregisterClass ( DirectScrapeBlockerProtocol . self)
204+ }
181205
182206 ProxyCannedProtocol . cannedJSON = Data ( )
183207 ProxyCannedProtocol . lastRequest = nil
208+ DirectScrapeBlockerProtocol . requestCount = 0
209+ defer { DirectScrapeBlockerProtocol . requestCount = 0 }
184210
185211 Defaults [ . useCheatProxy] = false
186212 Defaults [ . cheatProxyURL] = " https://test.proxy.pvemu.invalid "
@@ -202,6 +228,7 @@ final class GameHackingOrgLookupTests: XCTestCase {
202228private final class ProxyCannedProtocol : URLProtocol {
203229 static var cannedJSON : Data = Data ( )
204230 static var statusCode : Int = 200
231+ static var cannedHeaders : [ String : String ] = [ : ]
205232 static var lastRequest : URLRequest ?
206233
207234 override class func canInit( with request: URLRequest ) -> Bool {
@@ -212,11 +239,15 @@ private final class ProxyCannedProtocol: URLProtocol {
212239
213240 override func startLoading( ) {
214241 ProxyCannedProtocol . lastRequest = request
242+ var headers = [ " Content-Type " : " application/json " ]
243+ for (key, value) in ProxyCannedProtocol . cannedHeaders {
244+ headers [ key] = value
245+ }
215246 let response = HTTPURLResponse (
216247 url: request. url!,
217248 statusCode: ProxyCannedProtocol . statusCode,
218249 httpVersion: " HTTP/1.1 " ,
219- headerFields: [ " Content-Type " : " application/json " ]
250+ headerFields: headers
220251 ) !
221252 client? . urlProtocol ( self , didReceive: response, cacheStoragePolicy: . notAllowed)
222253 client? . urlProtocol ( self , didLoad: ProxyCannedProtocol . cannedJSON)
@@ -225,3 +256,30 @@ private final class ProxyCannedProtocol: URLProtocol {
225256
226257 override func stopLoading( ) { }
227258}
259+
260+ /// Intercepts requests to gamehacking.org and returns empty HTML, preventing real network calls
261+ /// during tests that exercise the direct-scrape fallback path.
262+ private final class DirectScrapeBlockerProtocol : URLProtocol {
263+ static var requestCount : Int = 0
264+
265+ override class func canInit( with request: URLRequest ) -> Bool {
266+ request. url? . host? . contains ( " gamehacking.org " ) ?? false
267+ }
268+
269+ override class func canonicalRequest( for request: URLRequest ) -> URLRequest { request }
270+
271+ override func startLoading( ) {
272+ DirectScrapeBlockerProtocol . requestCount += 1
273+ let response = HTTPURLResponse (
274+ url: request. url!,
275+ statusCode: 200 ,
276+ httpVersion: " HTTP/1.1 " ,
277+ headerFields: [ " Content-Type " : " text/html " ]
278+ ) !
279+ client? . urlProtocol ( self , didReceive: response, cacheStoragePolicy: . notAllowed)
280+ client? . urlProtocol ( self , didLoad: Data ( " <html><body></body></html> " . utf8) )
281+ client? . urlProtocolDidFinishLoading ( self )
282+ }
283+
284+ override func stopLoading( ) { }
285+ }
0 commit comments