@@ -116,6 +116,14 @@ class MockFeatureFlagManager: FeatureFlagManager {
116116 var fetchRequestCount = 0
117117 private var fetchStartTime : Date ?
118118
119+ // Request validation properties
120+ var requestValidationEnabled = false
121+ var lastRequestMethod : RequestMethod ?
122+ var lastRequestHeaders : [ String : String ] ?
123+ var lastRequestBody : Data ?
124+ var lastQueryItems : [ URLQueryItem ] ?
125+ var requestValidationError : String ?
126+
119127 // Override the now-internal method to prevent real network calls
120128 override func _performFetchRequest( ) {
121129 fetchRequestCount += 1
@@ -127,6 +135,11 @@ class MockFeatureFlagManager: FeatureFlagManager {
127135 self ? . fetchStartTime = startTime
128136 }
129137
138+ // If request validation is enabled, intercept and validate the request construction
139+ if requestValidationEnabled {
140+ validateRequestConstruction ( )
141+ }
142+
130143 // Instead of real network call, use simulated result
131144 if let result = simulatedFetchResult {
132145 if shouldSimulateNetworkDelay {
@@ -149,6 +162,56 @@ class MockFeatureFlagManager: FeatureFlagManager {
149162 }
150163 }
151164
165+ private func validateRequestConstruction( ) {
166+ // Clear previous validation error
167+ requestValidationError = nil
168+
169+ // Replicate the request construction logic from the real implementation to capture parameters
170+ guard let delegate = self . delegate else {
171+ requestValidationError = " Delegate missing "
172+ return
173+ }
174+ let options = delegate. getOptions ( )
175+
176+ let distinctId = delegate. getDistinctId ( )
177+ let anonymousId = delegate. getAnonymousId ( )
178+
179+ var context = options. featureFlagsContext
180+ context [ " distinct_id " ] = distinctId
181+ if let anonymousId = anonymousId {
182+ context [ " device_id " ] = anonymousId
183+ }
184+
185+ guard
186+ let contextData = try ? JSONSerialization . data (
187+ withJSONObject: context, options: [ ] ) ,
188+ let contextString = String ( data: contextData, encoding: . utf8)
189+ else {
190+ requestValidationError = " Failed to serialize context "
191+ return
192+ }
193+
194+ guard let authData = " \( options. token) : " . data ( using: . utf8) else {
195+ requestValidationError = " Failed to create auth data "
196+ return
197+ }
198+ let base64Auth = authData. base64EncodedString ( )
199+ let headers = [ " Authorization " : " Basic \( base64Auth) " ]
200+
201+ let queryItems = [
202+ URLQueryItem ( name: " context " , value: contextString) ,
203+ URLQueryItem ( name: " token " , value: options. token) ,
204+ URLQueryItem ( name: " mp_lib " , value: " swift " ) ,
205+ URLQueryItem ( name: " $lib_version " , value: AutomaticProperties . libVersion ( ) )
206+ ]
207+
208+ // Capture the constructed request parameters for validation
209+ lastRequestMethod = . get
210+ lastRequestHeaders = headers
211+ lastRequestBody = nil // GET request should have no body
212+ lastQueryItems = queryItems
213+ }
214+
152215 private func completeSimulatedFetch(
153216 success: Bool , flags: [ String : MixpanelFlagVariant ] ? , startTime: Date
154217 ) {
@@ -1427,4 +1490,148 @@ class FeatureFlagManagerTests: XCTestCase {
14271490 }
14281491 }
14291492
1493+ func testGETRequestFormat( ) {
1494+ // Use a fresh MockFeatureFlagManager with request validation enabled
1495+ let mockManager = MockFeatureFlagManager ( serverURL: " https://api.mixpanel.com " , delegate: mockDelegate)
1496+ mockManager. requestValidationEnabled = true
1497+ mockManager. simulatedFetchResult = ( success: true , flags: sampleFlags)
1498+
1499+ // Trigger a request
1500+ mockManager. loadFlags ( )
1501+
1502+ // Wait for request to be processed
1503+ let expectation = XCTestExpectation ( description: " Request validation completes " )
1504+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.2 ) { expectation. fulfill ( ) }
1505+ wait ( for: [ expectation] , timeout: 1.0 )
1506+
1507+ // Verify no validation errors
1508+ XCTAssertNil ( mockManager. requestValidationError, " Request validation should not have errors: \( mockManager. requestValidationError ?? " " ) " )
1509+
1510+ // Verify GET method
1511+ XCTAssertEqual ( mockManager. lastRequestMethod, . get, " Should use GET method " )
1512+
1513+ // Verify headers
1514+ XCTAssertNotNil ( mockManager. lastRequestHeaders, " Headers should be captured " )
1515+ if let headers = mockManager. lastRequestHeaders {
1516+ XCTAssertTrue ( headers. keys. contains ( " Authorization " ) , " Should include Authorization header " )
1517+ XCTAssertTrue ( headers [ " Authorization " ] ? . starts ( with: " Basic " ) == true , " Should use Basic auth " )
1518+ XCTAssertFalse ( headers. keys. contains ( " Content-Type " ) , " Should not include Content-Type header for GET " )
1519+ }
1520+
1521+ // Verify no request body
1522+ XCTAssertNil ( mockManager. lastRequestBody, " GET request should not have a body " )
1523+
1524+ // Verify query parameters
1525+ XCTAssertNotNil ( mockManager. lastQueryItems, " Query items should be captured " )
1526+ if let queryItems = mockManager. lastQueryItems {
1527+ let queryDict = Dictionary ( uniqueKeysWithValues: queryItems. map { ( $0. name, $0. value) } )
1528+
1529+ // Check required parameters
1530+ XCTAssertNotNil ( queryDict [ " context " ] , " Should include context parameter " )
1531+ XCTAssertEqual ( queryDict [ " token " ] , " test " , " Should include token parameter " )
1532+ XCTAssertEqual ( queryDict [ " mp_lib " ] , " swift " , " Should include mp_lib parameter " )
1533+ XCTAssertEqual ( queryDict [ " $lib_version " ] , AutomaticProperties . libVersion ( ) , " Should include $lib_version parameter " )
1534+
1535+ // Verify context JSON structure
1536+ if let contextString = queryDict [ " context " ] ,
1537+ let contextData = contextString? . data ( using: . utf8) ,
1538+ let context = try ? JSONSerialization . jsonObject ( with: contextData) as? [ String : Any ] {
1539+ XCTAssertEqual ( context [ " distinct_id " ] as? String , " test_distinct_id " , " Context should include distinct_id " )
1540+ XCTAssertEqual ( context [ " device_id " ] as? String , " test_anonymous_id " , " Context should include device_id " )
1541+ } else {
1542+ XCTFail ( " Context should be valid JSON " )
1543+ }
1544+ }
1545+ }
1546+
1547+ func testGETRequestWithCustomContext( ) {
1548+ // Set up custom context
1549+ let customOptions = MixpanelOptions ( token: " custom-token " , featureFlagsEnabled: true , featureFlagsContext: [
1550+ " user_id " : " test-user-123 " ,
1551+ " group_id " : " test-group-456 "
1552+ ] )
1553+
1554+ let customDelegate = MockFeatureFlagDelegate (
1555+ options: customOptions,
1556+ distinctId: " custom-distinct-id " ,
1557+ anonymousId: " custom-device-id "
1558+ )
1559+
1560+ let mockManager = MockFeatureFlagManager ( serverURL: " https://api.mixpanel.com " , delegate: customDelegate)
1561+ mockManager. requestValidationEnabled = true
1562+ mockManager. simulatedFetchResult = ( success: true , flags: sampleFlags)
1563+
1564+ // Trigger a request
1565+ mockManager. loadFlags ( )
1566+
1567+ // Wait for request to be processed
1568+ let expectation = XCTestExpectation ( description: " Custom context request validation " )
1569+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.2 ) { expectation. fulfill ( ) }
1570+ wait ( for: [ expectation] , timeout: 1.0 )
1571+
1572+ // Verify no validation errors
1573+ XCTAssertNil ( mockManager. requestValidationError, " Request validation should not have errors " )
1574+
1575+ // Verify query parameters with custom context
1576+ XCTAssertNotNil ( mockManager. lastQueryItems, " Query items should be captured " )
1577+ if let queryItems = mockManager. lastQueryItems {
1578+ let queryDict = Dictionary ( uniqueKeysWithValues: queryItems. map { ( $0. name, $0. value) } )
1579+ XCTAssertEqual ( queryDict [ " token " ] , " custom-token " , " Should include custom token " )
1580+ // Verify context includes both standard and custom fields
1581+ if let contextString = queryDict [ " context " ] ,
1582+ let contextData = contextString? . data ( using: . utf8) ,
1583+ let context = try ? JSONSerialization . jsonObject ( with: contextData) as? [ String : Any ] {
1584+
1585+ XCTAssertEqual ( context [ " distinct_id " ] as? String , " custom-distinct-id " , " Context should include distinct_id " )
1586+ XCTAssertEqual ( context [ " device_id " ] as? String , " custom-device-id " , " Context should include device_id " )
1587+ XCTAssertEqual ( context [ " user_id " ] as? String , " test-user-123 " , " Context should include custom user_id " )
1588+ XCTAssertEqual ( context [ " group_id " ] as? String , " test-group-456 " , " Context should include custom group_id " )
1589+ } else {
1590+ XCTFail ( " Context should be valid JSON with custom fields " )
1591+ }
1592+ }
1593+ }
1594+
1595+ func testGETRequestWithNilAnonymousId( ) {
1596+ // Set up with nil anonymous ID
1597+ let nilAnonymousDelegate = MockFeatureFlagDelegate (
1598+ options: MixpanelOptions ( token: " test-token " , featureFlagsEnabled: true ) ,
1599+ distinctId: " test-distinct-id " ,
1600+ anonymousId: nil
1601+ )
1602+
1603+ let mockManager = MockFeatureFlagManager ( serverURL: " https://api.mixpanel.com " , delegate: nilAnonymousDelegate)
1604+ mockManager. requestValidationEnabled = true
1605+ mockManager. simulatedFetchResult = ( success: true , flags: sampleFlags)
1606+
1607+ // Trigger a request
1608+ mockManager. loadFlags ( )
1609+
1610+ // Wait for request to be processed
1611+ let expectation = XCTestExpectation ( description: " Nil anonymous ID request validation " )
1612+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.2 ) { expectation. fulfill ( ) }
1613+ wait ( for: [ expectation] , timeout: 1.0 )
1614+
1615+ // Verify no validation errors
1616+ XCTAssertNil ( mockManager. requestValidationError, " Request validation should not have errors with nil anonymous ID " )
1617+
1618+ // Verify context excludes device_id when anonymous ID is nil
1619+ if let queryItems = mockManager. lastQueryItems {
1620+ let queryDict = Dictionary ( uniqueKeysWithValues: queryItems. map { ( $0. name, $0. value) } )
1621+
1622+ if let contextString = queryDict [ " context " ] ,
1623+ let contextData = contextString? . data ( using: . utf8) ,
1624+ let context = try ? JSONSerialization . jsonObject ( with: contextData) as? [ String : Any ] {
1625+
1626+ XCTAssertEqual ( context [ " distinct_id " ] as? String , " test-distinct-id " , " Context should include distinct_id " )
1627+ XCTAssertNil ( context [ " device_id " ] , " Context should not include device_id when anonymous ID is nil " )
1628+
1629+ // Should only contain distinct_id (no additional context configured)
1630+ XCTAssertEqual ( context. keys. count, 1 , " Context should only contain distinct_id when no device_id or additional context " )
1631+ } else {
1632+ XCTFail ( " Context should be valid JSON " )
1633+ }
1634+ }
1635+ }
1636+
14301637} // End Test Class
0 commit comments