Skip to content

Commit 9663216

Browse files
committed
Linter and Test issue fixes
1 parent a06e008 commit 9663216

9 files changed

+832
-535
lines changed

FlagsmithClient/Classes/Flagsmith.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ public final class Flagsmith: @unchecked Sendable {
270270

271271
// Check if we have a cache control header
272272
if let cacheControl = httpResponse.allHeaderFields["Cache-Control"] as? String {
273+
// First check for no-cache and no-store directives (case-insensitive, token-aware)
274+
if hasNoCacheDirective(in: cacheControl) {
275+
return false
276+
}
277+
273278
if let maxAge = extractMaxAge(from: cacheControl) {
274279
// Check if cache is still valid based on max-age
275280
if let dateString = httpResponse.allHeaderFields["Date"] as? String,
@@ -295,6 +300,18 @@ public final class Flagsmith: @unchecked Sendable {
295300
}
296301
return nil
297302
}
303+
304+
private func hasNoCacheDirective(in cacheControl: String) -> Bool {
305+
let components = cacheControl.split(separator: ",")
306+
for component in components {
307+
let trimmed = component.trimmingCharacters(in: .whitespaces)
308+
let directive = trimmed.lowercased()
309+
if directive == "no-cache" || directive == "no-store" {
310+
return true
311+
}
312+
}
313+
return false
314+
}
298315
}
299316

300317
// MARK: - HTTPURLResponse Extensions
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//
2+
// APIErrorCacheFallbackCoreTests.swift
3+
// FlagsmithClientTests
4+
//
5+
// Core API error scenarios with cache fallback behavior
6+
// Customer requirement: "When fetching flags and we run into an error and have a valid cache we should return the cached flags"
7+
//
8+
9+
@testable import FlagsmithClient
10+
import XCTest
11+
12+
final class APIErrorCacheFallbackCoreTests: FlagsmithClientTestCase {
13+
var testCache: URLCache!
14+
15+
override func setUp() {
16+
super.setUp()
17+
18+
// Create isolated cache for testing
19+
testCache = URLCache(memoryCapacity: 8 * 1024 * 1024, diskCapacity: 64 * 1024 * 1024, directory: nil)
20+
21+
// Reset Flagsmith to known state using TestConfig
22+
Flagsmith.shared.apiKey = TestConfig.hasRealApiKey ? TestConfig.apiKey : "mock-test-api-key"
23+
Flagsmith.shared.baseURL = TestConfig.baseURL
24+
Flagsmith.shared.enableRealtimeUpdates = false
25+
Flagsmith.shared.cacheConfig.useCache = true
26+
Flagsmith.shared.cacheConfig.skipAPI = false
27+
Flagsmith.shared.cacheConfig.cache = testCache
28+
Flagsmith.shared.cacheConfig.cacheTTL = 300
29+
Flagsmith.shared.defaultFlags = []
30+
}
31+
32+
override func tearDown() {
33+
testCache.removeAllCachedResponses()
34+
Flagsmith.shared.cacheConfig.useCache = false
35+
Flagsmith.shared.cacheConfig.skipAPI = false
36+
Flagsmith.shared.apiKey = nil
37+
super.tearDown()
38+
}
39+
40+
// MARK: - Test Helper Methods
41+
42+
private func createMockCachedResponse(for request: URLRequest, with flags: [Flag]) throws -> CachedURLResponse {
43+
let jsonData = try JSONEncoder().encode(flags)
44+
let httpResponse = HTTPURLResponse(
45+
url: request.url!,
46+
statusCode: 200,
47+
httpVersion: "HTTP/1.1",
48+
headerFields: [
49+
"Content-Type": "application/json",
50+
"Cache-Control": "max-age=300"
51+
]
52+
)!
53+
return CachedURLResponse(response: httpResponse, data: jsonData)
54+
}
55+
56+
// MARK: - Core API Error Cache Fallback Tests
57+
58+
func testGetFeatureFlags_APIFailure_ReturnsCachedFlags() throws {
59+
// This test works with mock data, no real API key needed
60+
let expectation = expectation(description: "API failure with cache fallback")
61+
62+
// Create mock flags for cache
63+
let cachedFlags = [
64+
Flag(featureName: "cached_feature_1", value: .string("cached_value_1"), enabled: true, featureType: "FLAG"),
65+
Flag(featureName: "cached_feature_2", value: .string("cached_value_2"), enabled: false, featureType: "FLAG")
66+
]
67+
68+
// Pre-populate cache with successful response
69+
var mockRequest = URLRequest(url: TestConfig.baseURL.appendingPathComponent("flags/"))
70+
mockRequest.setValue(TestConfig.apiKey, forHTTPHeaderField: "X-Environment-Key")
71+
let cachedResponse = try createMockCachedResponse(for: mockRequest, with: cachedFlags)
72+
testCache.storeCachedResponse(cachedResponse, for: mockRequest)
73+
74+
// Mock API failure by using invalid API key
75+
Flagsmith.shared.apiKey = "invalid-api-key"
76+
77+
// Request should fail API call but return cached flags
78+
Flagsmith.shared.getFeatureFlags { result in
79+
switch result {
80+
case .success(let flags):
81+
// Should return cached flags
82+
XCTAssertEqual(flags.count, 2, "Should return cached flags")
83+
XCTAssertEqual(flags.first?.feature.name, "cached_feature_1", "Should return first cached flag")
84+
XCTAssertEqual(flags.last?.feature.name, "cached_feature_2", "Should return second cached flag")
85+
case .failure(let error):
86+
XCTFail("Should return cached flags instead of failing: \(error)")
87+
}
88+
expectation.fulfill()
89+
}
90+
91+
wait(for: [expectation], timeout: 5.0)
92+
}
93+
94+
func testGetFeatureFlags_APIFailure_NoCache_ReturnsDefaultFlags() throws {
95+
// This test works with mock data, no real API key needed
96+
let expectation = expectation(description: "API failure with no cache, default flags fallback")
97+
98+
// Set up default flags
99+
let defaultFlags = [
100+
Flag(featureName: "default_feature", value: .string("default_value"), enabled: true, featureType: "FLAG")
101+
]
102+
Flagsmith.shared.defaultFlags = defaultFlags
103+
104+
// Ensure no cache exists
105+
testCache.removeAllCachedResponses()
106+
107+
// Mock API failure
108+
Flagsmith.shared.apiKey = "invalid-api-key"
109+
110+
// Request should fail API call and return default flags
111+
Flagsmith.shared.getFeatureFlags { result in
112+
switch result {
113+
case .success(let flags):
114+
// Should return default flags
115+
XCTAssertEqual(flags.count, 1, "Should return default flags")
116+
XCTAssertEqual(flags.first?.feature.name, "default_feature", "Should return default flag")
117+
case .failure(let error):
118+
XCTFail("Should return default flags instead of failing: \(error)")
119+
}
120+
expectation.fulfill()
121+
}
122+
123+
wait(for: [expectation], timeout: 5.0)
124+
}
125+
126+
func testGetFeatureFlags_APIFailure_NoCacheNoDefaults_ReturnsError() throws {
127+
// This test works with mock data, no real API key needed
128+
let expectation = expectation(description: "API failure with no cache and no defaults")
129+
130+
// Ensure no cache and no defaults
131+
testCache.removeAllCachedResponses()
132+
Flagsmith.shared.defaultFlags = []
133+
134+
// Mock API failure
135+
Flagsmith.shared.apiKey = "invalid-api-key"
136+
137+
// Request should fail
138+
Flagsmith.shared.getFeatureFlags { result in
139+
switch result {
140+
case .success(_):
141+
XCTFail("Should fail when no cache and no defaults")
142+
case .failure(let error):
143+
// Should return the original API error
144+
XCTAssertTrue(error is FlagsmithError, "Should return FlagsmithError")
145+
}
146+
expectation.fulfill()
147+
}
148+
149+
wait(for: [expectation], timeout: 5.0)
150+
}
151+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// APIErrorCacheFallbackEdgeCaseTests.swift
3+
// FlagsmithClientTests
4+
//
5+
// Edge case API error scenarios with cache fallback behavior
6+
//
7+
8+
@testable import FlagsmithClient
9+
import XCTest
10+
11+
final class APIErrorCacheFallbackEdgeCaseTests: FlagsmithClientTestCase {
12+
var testCache: URLCache!
13+
14+
override func setUp() {
15+
super.setUp()
16+
17+
// Create isolated cache for testing
18+
testCache = URLCache(memoryCapacity: 8 * 1024 * 1024, diskCapacity: 64 * 1024 * 1024, directory: nil)
19+
20+
// Reset Flagsmith to known state using TestConfig
21+
Flagsmith.shared.apiKey = TestConfig.hasRealApiKey ? TestConfig.apiKey : "mock-test-api-key"
22+
Flagsmith.shared.baseURL = TestConfig.baseURL
23+
Flagsmith.shared.enableRealtimeUpdates = false
24+
Flagsmith.shared.cacheConfig.useCache = true
25+
Flagsmith.shared.cacheConfig.skipAPI = false
26+
Flagsmith.shared.cacheConfig.cache = testCache
27+
Flagsmith.shared.cacheConfig.cacheTTL = 300
28+
Flagsmith.shared.defaultFlags = []
29+
}
30+
31+
override func tearDown() {
32+
testCache.removeAllCachedResponses()
33+
Flagsmith.shared.cacheConfig.useCache = false
34+
Flagsmith.shared.cacheConfig.skipAPI = false
35+
Flagsmith.shared.apiKey = nil
36+
super.tearDown()
37+
}
38+
39+
// MARK: - Edge Case Tests
40+
41+
func testCacheFallback_CorruptedCache_ReturnsDefaultFlags() throws {
42+
// This test works with mock data, no real API key needed
43+
let expectation = expectation(description: "Corrupted cache with default flags fallback")
44+
45+
// Create corrupted cache entry
46+
let corruptedData = "invalid json data".data(using: .utf8)!
47+
var mockRequest = URLRequest(url: TestConfig.baseURL.appendingPathComponent("flags/"))
48+
mockRequest.setValue(TestConfig.apiKey, forHTTPHeaderField: "X-Environment-Key")
49+
50+
let httpResponse = HTTPURLResponse(
51+
url: mockRequest.url!,
52+
statusCode: 200,
53+
httpVersion: "HTTP/1.1",
54+
headerFields: [
55+
"Content-Type": "application/json",
56+
"Cache-Control": "max-age=300"
57+
]
58+
)!
59+
60+
let corruptedCachedResponse = CachedURLResponse(response: httpResponse, data: corruptedData)
61+
testCache.storeCachedResponse(corruptedCachedResponse, for: mockRequest)
62+
63+
// Set up default flags
64+
let defaultFlags = [
65+
Flag(featureName: "default_feature", value: .string("default_value"), enabled: true, featureType: "FLAG")
66+
]
67+
Flagsmith.shared.defaultFlags = defaultFlags
68+
69+
// Mock API failure
70+
Flagsmith.shared.apiKey = "invalid-api-key"
71+
72+
// Request should fail API call and return default flags (not corrupted cache)
73+
Flagsmith.shared.getFeatureFlags { result in
74+
switch result {
75+
case .success(let flags):
76+
// Should return default flags, not corrupted cache
77+
XCTAssertEqual(flags.count, 1, "Should return default flags")
78+
XCTAssertEqual(flags.first?.feature.name, "default_feature", "Should return default flag, not corrupted cache")
79+
case .failure(let error):
80+
XCTFail("Should return default flags instead of failing: \(error)")
81+
}
82+
expectation.fulfill()
83+
}
84+
85+
wait(for: [expectation], timeout: 5.0)
86+
}
87+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//
2+
// APIErrorCacheFallbackErrorTests.swift
3+
// FlagsmithClientTests
4+
//
5+
// Different error type API error scenarios with cache fallback behavior
6+
//
7+
8+
@testable import FlagsmithClient
9+
import XCTest
10+
11+
final class APIErrorCacheFallbackErrorTests: FlagsmithClientTestCase {
12+
var testCache: URLCache!
13+
14+
override func setUp() {
15+
super.setUp()
16+
17+
// Create isolated cache for testing
18+
testCache = URLCache(memoryCapacity: 8 * 1024 * 1024, diskCapacity: 64 * 1024 * 1024, directory: nil)
19+
20+
// Reset Flagsmith to known state using TestConfig
21+
Flagsmith.shared.apiKey = TestConfig.hasRealApiKey ? TestConfig.apiKey : "mock-test-api-key"
22+
Flagsmith.shared.baseURL = TestConfig.baseURL
23+
Flagsmith.shared.enableRealtimeUpdates = false
24+
Flagsmith.shared.cacheConfig.useCache = true
25+
Flagsmith.shared.cacheConfig.skipAPI = false
26+
Flagsmith.shared.cacheConfig.cache = testCache
27+
Flagsmith.shared.cacheConfig.cacheTTL = 300
28+
Flagsmith.shared.defaultFlags = []
29+
}
30+
31+
override func tearDown() {
32+
testCache.removeAllCachedResponses()
33+
Flagsmith.shared.cacheConfig.useCache = false
34+
Flagsmith.shared.cacheConfig.skipAPI = false
35+
Flagsmith.shared.apiKey = nil
36+
super.tearDown()
37+
}
38+
39+
// MARK: - Test Helper Methods
40+
41+
private func createMockCachedResponse(for request: URLRequest, with flags: [Flag]) throws -> CachedURLResponse {
42+
let jsonData = try JSONEncoder().encode(flags)
43+
let httpResponse = HTTPURLResponse(
44+
url: request.url!,
45+
statusCode: 200,
46+
httpVersion: "HTTP/1.1",
47+
headerFields: [
48+
"Content-Type": "application/json",
49+
"Cache-Control": "max-age=300"
50+
]
51+
)!
52+
return CachedURLResponse(response: httpResponse, data: jsonData)
53+
}
54+
55+
// MARK: - Different Error Type Tests
56+
57+
func testCacheFallback_NetworkError_ReturnsCachedFlags() throws {
58+
// This test works with mock data, no real API key needed
59+
let expectation = expectation(description: "Network error with cache fallback")
60+
61+
// Create cached flags
62+
let cachedFlags = [
63+
Flag(featureName: "network_cached_feature", value: .string("network_cached_value"), enabled: true, featureType: "FLAG")
64+
]
65+
66+
// Pre-populate cache
67+
var mockRequest = URLRequest(url: TestConfig.baseURL.appendingPathComponent("flags/"))
68+
mockRequest.setValue(TestConfig.apiKey, forHTTPHeaderField: "X-Environment-Key")
69+
let cachedResponse = try createMockCachedResponse(for: mockRequest, with: cachedFlags)
70+
testCache.storeCachedResponse(cachedResponse, for: mockRequest)
71+
72+
// Simulate network error by using invalid API key (this will cause API failure)
73+
Flagsmith.shared.apiKey = "invalid-api-key"
74+
75+
// Request should fail API call but return cached flags
76+
Flagsmith.shared.getFeatureFlags { result in
77+
switch result {
78+
case .success(let flags):
79+
// Should return cached flags
80+
XCTAssertEqual(flags.count, 1, "Should return cached flags")
81+
XCTAssertEqual(flags.first?.feature.name, "network_cached_feature", "Should return cached flag")
82+
case .failure(let error):
83+
XCTFail("Should return cached flags instead of failing: \(error)")
84+
}
85+
expectation.fulfill()
86+
}
87+
88+
wait(for: [expectation], timeout: 5.0)
89+
}
90+
91+
func testCacheFallback_ServerError_ReturnsCachedFlags() throws {
92+
// This test works with mock data, no real API key needed
93+
let expectation = expectation(description: "Server error with cache fallback")
94+
95+
// Create cached flags
96+
let cachedFlags = [
97+
Flag(featureName: "server_cached_feature", value: .string("server_cached_value"), enabled: true, featureType: "FLAG")
98+
]
99+
100+
// Pre-populate cache
101+
var mockRequest = URLRequest(url: TestConfig.baseURL.appendingPathComponent("flags/"))
102+
mockRequest.setValue(TestConfig.apiKey, forHTTPHeaderField: "X-Environment-Key")
103+
let cachedResponse = try createMockCachedResponse(for: mockRequest, with: cachedFlags)
104+
testCache.storeCachedResponse(cachedResponse, for: mockRequest)
105+
106+
// Simulate server error by using invalid API key (this will cause API failure)
107+
Flagsmith.shared.apiKey = "invalid-api-key"
108+
109+
// Request should fail API call but return cached flags
110+
Flagsmith.shared.getFeatureFlags { result in
111+
switch result {
112+
case .success(let flags):
113+
// Should return cached flags
114+
XCTAssertEqual(flags.count, 1, "Should return cached flags")
115+
XCTAssertEqual(flags.first?.feature.name, "server_cached_feature", "Should return cached flag")
116+
case .failure(let error):
117+
XCTFail("Should return cached flags instead of failing: \(error)")
118+
}
119+
expectation.fulfill()
120+
}
121+
122+
wait(for: [expectation], timeout: 5.0)
123+
}
124+
}

0 commit comments

Comments
 (0)