@@ -2,14 +2,42 @@ import XCTest
22@testable import VPNBypassCore
33import Foundation
44
5+ // Base class that snapshots and restores the real config FILE around each test,
6+ // preventing tests from corrupting the user's production config.json.
7+ @MainActor
8+ class RouteManagerTestCase : XCTestCase {
9+
10+ var rm : RouteManager { RouteManager . shared }
11+
12+ private static let configDir : URL = {
13+ let appSupport = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) . first!
14+ return appSupport. appendingPathComponent ( " VPNBypass " , isDirectory: true )
15+ } ( )
16+ private static let configURL = configDir. appendingPathComponent ( " config.json " )
17+ private static let testBackupURL = configDir. appendingPathComponent ( " config.json.test-snapshot " )
18+
19+ override func setUp( ) {
20+ super. setUp ( )
21+ try ? FileManager . default. copyItem ( at: Self . configURL, to: Self . testBackupURL)
22+ }
23+
24+ override func tearDown( ) {
25+ let fm = FileManager . default
26+ if fm. fileExists ( atPath: Self . testBackupURL. path) {
27+ try ? fm. removeItem ( at: Self . configURL)
28+ try ? fm. moveItem ( at: Self . testBackupURL, to: Self . configURL)
29+ rm. loadConfig ( )
30+ }
31+ super. tearDown ( )
32+ }
33+ }
34+
535// Tests that exercise RouteManager public methods which modify config state.
636// Since isVPNConnected is false in tests, route application branches are skipped,
737// but config mutations, logging, and validation logic all execute.
838
939@MainActor
10- final class AddDomainTests : XCTestCase {
11-
12- private var rm : RouteManager { RouteManager . shared }
40+ final class AddDomainTests : RouteManagerTestCase {
1341
1442 override func setUp( ) {
1543 super. setUp ( )
@@ -53,9 +81,7 @@ final class AddDomainTests: XCTestCase {
5381}
5482
5583@MainActor
56- final class RemoveDomainTests : XCTestCase {
57-
58- private var rm : RouteManager { RouteManager . shared }
84+ final class RemoveDomainTests : RouteManagerTestCase {
5985
6086 override func setUp( ) {
6187 super. setUp ( )
@@ -69,16 +95,13 @@ final class RemoveDomainTests: XCTestCase {
6995 return
7096 }
7197 rm. removeDomain ( entry)
72- // removeDomain dispatches an async Task; give it time to complete
7398 try await Task . sleep ( nanoseconds: 200_000_000 )
7499 XCTAssertFalse ( rm. config. domains. contains ( where: { $0. domain == " to-remove.com " } ) )
75100 }
76101}
77102
78103@MainActor
79- final class ToggleDomainTests : XCTestCase {
80-
81- private var rm : RouteManager { RouteManager . shared }
104+ final class ToggleDomainTests : RouteManagerTestCase {
82105
83106 override func setUp( ) {
84107 super. setUp ( )
@@ -111,9 +134,7 @@ final class ToggleDomainTests: XCTestCase {
111134}
112135
113136@MainActor
114- final class SetAllDomainsEnabledTests : XCTestCase {
115-
116- private var rm : RouteManager { RouteManager . shared }
137+ final class SetAllDomainsEnabledTests : RouteManagerTestCase {
117138
118139 override func setUp( ) {
119140 super. setUp ( )
@@ -141,9 +162,7 @@ final class SetAllDomainsEnabledTests: XCTestCase {
141162}
142163
143164@MainActor
144- final class AddInverseDomainIntegrationTests : XCTestCase {
145-
146- private var rm : RouteManager { RouteManager . shared }
165+ final class AddInverseDomainIntegrationTests : RouteManagerTestCase {
147166
148167 override func setUp( ) {
149168 super. setUp ( )
@@ -181,9 +200,7 @@ final class AddInverseDomainIntegrationTests: XCTestCase {
181200}
182201
183202@MainActor
184- final class ToggleInverseDomainTests : XCTestCase {
185-
186- private var rm : RouteManager { RouteManager . shared }
203+ final class ToggleInverseDomainTests : RouteManagerTestCase {
187204
188205 override func setUp( ) {
189206 super. setUp ( )
@@ -203,9 +220,7 @@ final class ToggleInverseDomainTests: XCTestCase {
203220}
204221
205222@MainActor
206- final class SetAllInverseDomainsTests : XCTestCase {
207-
208- private var rm : RouteManager { RouteManager . shared }
223+ final class SetAllInverseDomainsTests : RouteManagerTestCase {
209224
210225 override func setUp( ) {
211226 super. setUp ( )
@@ -232,9 +247,7 @@ final class SetAllInverseDomainsTests: XCTestCase {
232247}
233248
234249@MainActor
235- final class CustomServiceTests : XCTestCase {
236-
237- private var rm : RouteManager { RouteManager . shared }
250+ final class CustomServiceTests : RouteManagerTestCase {
238251
239252 override func setUp( ) {
240253 super. setUp ( )
@@ -263,7 +276,6 @@ final class CustomServiceTests: XCTestCase {
263276 return
264277 }
265278 rm. removeCustomService ( svc. id)
266- // removeCustomService dispatches an async Task; give it time to complete
267279 try await Task . sleep ( nanoseconds: 200_000_000 )
268280 XCTAssertFalse ( rm. config. services. contains ( where: { $0. id == svc. id } ) )
269281 }
@@ -283,9 +295,7 @@ final class CustomServiceTests: XCTestCase {
283295}
284296
285297@MainActor
286- final class ToggleServiceTests : XCTestCase {
287-
288- private var rm : RouteManager { RouteManager . shared }
298+ final class ToggleServiceTests : RouteManagerTestCase {
289299
290300 func testToggleBuiltInService( ) {
291301 guard let first = rm. config. services. first else {
@@ -301,9 +311,7 @@ final class ToggleServiceTests: XCTestCase {
301311}
302312
303313@MainActor
304- final class SetRoutingModeTests : XCTestCase {
305-
306- private var rm : RouteManager { RouteManager . shared }
314+ final class SetRoutingModeTests : RouteManagerTestCase {
307315
308316 func testSetRoutingModeToVPNOnly( ) {
309317 rm. setRoutingMode ( . vpnOnly)
@@ -325,9 +333,7 @@ final class SetRoutingModeTests: XCTestCase {
325333}
326334
327335@MainActor
328- final class ExportImportConfigTests : XCTestCase {
329-
330- private var rm : RouteManager { RouteManager . shared }
336+ final class ExportImportConfigTests : RouteManagerTestCase {
331337
332338 func testExportConfigReturnsURL( ) {
333339 let url = rm. exportConfig ( )
@@ -372,9 +378,7 @@ final class ExportImportConfigTests: XCTestCase {
372378}
373379
374380@MainActor
375- final class SaveLoadConfigTests : XCTestCase {
376-
377- private var rm : RouteManager { RouteManager . shared }
381+ final class SaveLoadConfigTests : RouteManagerTestCase {
378382
379383 func testSaveConfigDoesNotCrash( ) {
380384 rm. saveConfig ( )
@@ -390,8 +394,6 @@ final class SaveLoadConfigTests: XCTestCase {
390394 rm. config. checkInterval = 0
391395 rm. loadConfig ( )
392396 XCTAssertEqual ( rm. config. checkInterval, 999 )
393- rm. config. checkInterval = 300
394- rm. saveConfig ( )
395397 }
396398}
397399
@@ -400,7 +402,6 @@ final class DetectedDNSDisplayTests: XCTestCase {
400402
401403 func testDetectedDNSServerDisplayDefaultNil( ) {
402404 let display = RouteManager . shared. detectedDNSServerDisplay
403- // May or may not be nil depending on state, just check it doesn't crash
404405 _ = display
405406 }
406407}
0 commit comments