Skip to content

Commit 965004f

Browse files
committed
fix(tests): prevent integration tests from overwriting production config
Tests operated on RouteManager.shared which writes to the real config.json. Added RouteManagerTestCase base class that snapshots and restores the config file around each test class.
1 parent 0b4fd69 commit 965004f

1 file changed

Lines changed: 42 additions & 41 deletions

File tree

Tests/VPNBypassTests/RouteManagerIntegrationTests.swift

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,42 @@ import XCTest
22
@testable import VPNBypassCore
33
import 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

Comments
 (0)