@@ -170,6 +170,236 @@ class CountlyConnectionManagerTests: CountlyBaseTestCase {
170170 XCTAssertEqual ( captured? . count, 1 )
171171 }
172172
173+ /**
174+ * <pre>
175+ * Test that all outgoing requests use GET method by default
176+ * when alwaysUsePOST is false and query strings are short.
177+ *
178+ * 1- Init SDK with MockURLProtocol, alwaysUsePOST = false
179+ * 2- Call all possible SDK functions: sessions, events, user details,
180+ * location, direct request, attribution, views, crashes, APM, remote config, rating, feedbacks
181+ * 3- Wait for requests to be sent
182+ * 4- Verify all intercepted requests use GET method
183+ * 5- Verify request URL contains query string (no HTTP body)
184+ * </pre>
185+ */
186+ func test_allRequests_useGET_whenAlwaysUsePOSTDisabled( ) {
187+ let expectation = self . expectation ( description: " Requests intercepted " )
188+ expectation. assertForOverFulfill = false
189+
190+ let expectedMinimumRequests = 10
191+ var capturedRequests : [ URLRequest ] = [ ]
192+ let lock = NSLock ( )
193+ MockURLProtocol . requestHandler = { request in
194+ lock. lock ( )
195+ capturedRequests. append ( request)
196+ if capturedRequests. count >= expectedMinimumRequests {
197+ expectation. fulfill ( )
198+ }
199+ lock. unlock ( )
200+ let response = HTTPURLResponse ( url: request. url!, statusCode: 200 , httpVersion: " HTTP/1.1 " , headerFields: nil ) !
201+ let body = " { \" result \" :[]} " . data ( using: . utf8) !
202+ return ( body, response, nil )
203+ }
204+
205+ let config = createBaseConfig ( )
206+ config. alwaysUsePOST = false
207+ config. manualSessionHandling = true
208+ config. disableSDKBehaviorSettingsUpdates = true
209+ config. enablePerformanceMonitoring = true
210+ let sessionConfig = URLSessionConfiguration . default
211+ sessionConfig. protocolClasses = [ MockURLProtocol . self]
212+ config. urlSessionConfiguration = sessionConfig
213+
214+ Countly . sharedInstance ( ) . start ( with: config)
215+
216+ // Session begin
217+ Countly . sharedInstance ( ) . beginSession ( )
218+
219+ // Events
220+ Countly . sharedInstance ( ) . recordEvent ( " test_event_1 " )
221+ Countly . sharedInstance ( ) . recordEvent ( " test_event_2 " , segmentation: [ " key " : " value " ] , count: 1 , sum: 1.5 , duration: 10 )
222+
223+ // User details
224+ Countly . user ( ) . set ( " custom_property " , value: " custom_value " )
225+ Countly . user ( ) . increment ( " login_count " )
226+ Countly . user ( ) . push ( " tags " , value: " vip " )
227+ Countly . user ( ) . save ( )
228+
229+ // Location
230+ Countly . sharedInstance ( ) . recordLocation ( CLLocationCoordinate2D ( latitude: 41.0082 , longitude: 28.9784 ) , city: " Istanbul " , isoCountryCode: " TR " , ip: " 1.2.3.4 " )
231+
232+ // Direct request
233+ Countly . sharedInstance ( ) . addDirectRequest ( [ " custom_key " : " custom_value " ] )
234+
235+ // Attribution
236+ Countly . sharedInstance ( ) . recordDirectAttribution ( withCampaignType: " _special_test " , andCampaignData: " { \" cid \" : \" test_campaign \" } " )
237+ Countly . sharedInstance ( ) . recordIndirectAttribution ( [ " idfa " : " test_idfa_value " ] )
238+
239+ // Views
240+ let _ = Countly . sharedInstance ( ) . views ( ) . startAutoStoppedView ( " TestView " )
241+ let _ = Countly . sharedInstance ( ) . views ( ) . startView ( " TestView2 " )
242+ Countly . sharedInstance ( ) . views ( ) . stopView ( withName: " TestView2 " )
243+
244+ // Crash/Error reporting
245+ Countly . sharedInstance ( ) . recordError ( " TestError " , isFatal: false , stackTrace: [ " frame1 " , " frame2 " ] , segmentation: [ " errorType " : " test " ] )
246+
247+ // Performance monitoring (APM)
248+ Countly . sharedInstance ( ) . recordNetworkTrace ( " test_trace " , requestPayloadSize: 100 , responsePayloadSize: 200 , responseStatusCode: 200 , startTime: 1111 , endTime: 2222 )
249+
250+ // Remote config
251+ Countly . sharedInstance ( ) . remoteConfig ( ) . downloadKeys { _, _, _, _ in }
252+ Countly . sharedInstance ( ) . remoteConfig ( ) . enrollIntoABTests ( forKeys: [ " test_key " ] )
253+ Countly . sharedInstance ( ) . remoteConfig ( ) . exitABTests ( forKeys: [ " test_key " ] )
254+
255+ // Feedbacks and Rating (iOS only)
256+ #if os(iOS)
257+ Countly . sharedInstance ( ) . recordRatingWidget ( withID: " test_widget_id " , rating: 5 , email: " test@test.com " , comment: " Great " , userCanBeContacted: true )
258+ Countly . sharedInstance ( ) . feedback ( ) . getAvailableFeedbackWidgets { _, _ in }
259+ #endif
260+
261+ // Session update (flushes queued events)
262+ Countly . sharedInstance ( ) . updateSession ( )
263+
264+ // Session end
265+ Countly . sharedInstance ( ) . endSession ( )
266+
267+ waitForExpectations ( timeout: 15 )
268+
269+ XCTAssertGreaterThanOrEqual ( capturedRequests. count, expectedMinimumRequests, " Should have captured at least \( expectedMinimumRequests) requests from various SDK functions " )
270+ for request in capturedRequests {
271+ XCTAssertEqual ( request. httpMethod, " GET " , " Request to \( request. url? . path ?? " " ) should use GET " )
272+ XCTAssertTrue ( request. url? . query? . isEmpty == false , " GET request should have query string in URL " )
273+ }
274+ }
275+
276+ /**
277+ * <pre>
278+ * Test that all outgoing requests use POST method
279+ * when alwaysUsePOST config flag is enabled.
280+ *
281+ * 1- Init SDK with MockURLProtocol, alwaysUsePOST = true
282+ * 2- Call all possible SDK functions: sessions, events, user details,
283+ * location, direct request, attribution, views, crashes, APM, remote config, rating, feedbacks
284+ * 3- Wait for requests to be sent
285+ * 4- Verify all intercepted requests use POST method
286+ * 5- Verify requests have HTTP body and no query string in URL
287+ * </pre>
288+ */
289+ func test_allRequests_usePOST_whenAlwaysUsePOSTEnabled( ) {
290+ let expectation = self . expectation ( description: " Requests intercepted " )
291+ expectation. assertForOverFulfill = false
292+
293+ let expectedMinimumRequests = 10
294+ var capturedRequests : [ URLRequest ] = [ ]
295+ var capturedBodies : [ Data ] = [ ]
296+ let lock = NSLock ( )
297+ MockURLProtocol . requestHandler = { request in
298+ lock. lock ( )
299+ capturedRequests. append ( request)
300+ // Read body from stream if needed
301+ if let body = request. httpBody {
302+ capturedBodies. append ( body)
303+ } else if let stream = request. httpBodyStream {
304+ stream. open ( )
305+ let bufferSize = 4096
306+ let buffer = UnsafeMutablePointer< UInt8> . allocate( capacity: bufferSize)
307+ var data = Data ( )
308+ while stream. hasBytesAvailable {
309+ let read = stream. read ( buffer, maxLength: bufferSize)
310+ if read > 0 {
311+ data. append ( buffer, count: read)
312+ }
313+ }
314+ buffer. deallocate ( )
315+ stream. close ( )
316+ capturedBodies. append ( data)
317+ }
318+ if capturedRequests. count >= expectedMinimumRequests {
319+ expectation. fulfill ( )
320+ }
321+ lock. unlock ( )
322+ let response = HTTPURLResponse ( url: request. url!, statusCode: 200 , httpVersion: " HTTP/1.1 " , headerFields: nil ) !
323+ let body = " { \" result \" :[]} " . data ( using: . utf8) !
324+ return ( body, response, nil )
325+ }
326+
327+ let config = createBaseConfig ( )
328+ config. alwaysUsePOST = true
329+ config. manualSessionHandling = true
330+ config. disableSDKBehaviorSettingsUpdates = true
331+ config. enablePerformanceMonitoring = true
332+ let sessionConfig = URLSessionConfiguration . default
333+ sessionConfig. protocolClasses = [ MockURLProtocol . self]
334+ config. urlSessionConfiguration = sessionConfig
335+
336+ Countly . sharedInstance ( ) . start ( with: config)
337+
338+ // Session begin
339+ Countly . sharedInstance ( ) . beginSession ( )
340+
341+ // Events
342+ Countly . sharedInstance ( ) . recordEvent ( " test_event_1 " )
343+ Countly . sharedInstance ( ) . recordEvent ( " test_event_2 " , segmentation: [ " key " : " value " ] , count: 1 , sum: 1.5 , duration: 10 )
344+
345+ // User details
346+ Countly . user ( ) . set ( " custom_property " , value: " custom_value " )
347+ Countly . user ( ) . increment ( " login_count " )
348+ Countly . user ( ) . push ( " tags " , value: " vip " )
349+ Countly . user ( ) . save ( )
350+
351+ // Location
352+ Countly . sharedInstance ( ) . recordLocation ( CLLocationCoordinate2D ( latitude: 41.0082 , longitude: 28.9784 ) , city: " Istanbul " , isoCountryCode: " TR " , ip: " 1.2.3.4 " )
353+
354+ // Direct request
355+ Countly . sharedInstance ( ) . addDirectRequest ( [ " custom_key " : " custom_value " ] )
356+
357+ // Attribution
358+ Countly . sharedInstance ( ) . recordDirectAttribution ( withCampaignType: " _special_test " , andCampaignData: " { \" cid \" : \" test_campaign \" } " )
359+ Countly . sharedInstance ( ) . recordIndirectAttribution ( [ " idfa " : " test_idfa_value " ] )
360+
361+ // Views
362+ let _ = Countly . sharedInstance ( ) . views ( ) . startAutoStoppedView ( " TestView " )
363+ let _ = Countly . sharedInstance ( ) . views ( ) . startView ( " TestView2 " )
364+ Countly . sharedInstance ( ) . views ( ) . stopView ( withName: " TestView2 " )
365+
366+ // Crash/Error reporting
367+ Countly . sharedInstance ( ) . recordError ( " TestError " , isFatal: false , stackTrace: [ " frame1 " , " frame2 " ] , segmentation: [ " errorType " : " test " ] )
368+
369+ // Performance monitoring (APM)
370+ Countly . sharedInstance ( ) . recordNetworkTrace ( " test_trace " , requestPayloadSize: 100 , responsePayloadSize: 200 , responseStatusCode: 200 , startTime: 1111 , endTime: 2222 )
371+
372+ // Remote config
373+ Countly . sharedInstance ( ) . remoteConfig ( ) . downloadKeys { _, _, _, _ in }
374+ Countly . sharedInstance ( ) . remoteConfig ( ) . enrollIntoABTests ( forKeys: [ " test_key " ] )
375+ Countly . sharedInstance ( ) . remoteConfig ( ) . exitABTests ( forKeys: [ " test_key " ] )
376+
377+ // Feedbacks and Rating (iOS only)
378+ #if os(iOS)
379+ Countly . sharedInstance ( ) . recordRatingWidget ( withID: " test_widget_id " , rating: 5 , email: " test@test.com " , comment: " Great " , userCanBeContacted: true )
380+ Countly . sharedInstance ( ) . feedback ( ) . getAvailableFeedbackWidgets { _, _ in }
381+ #endif
382+
383+ // Session update (flushes queued events)
384+ Countly . sharedInstance ( ) . updateSession ( )
385+
386+ // Session end
387+ Countly . sharedInstance ( ) . endSession ( )
388+
389+ waitForExpectations ( timeout: 15 )
390+
391+ XCTAssertGreaterThanOrEqual ( capturedRequests. count, expectedMinimumRequests, " Should have captured at least \( expectedMinimumRequests) requests from various SDK functions " )
392+ for (index, request) in capturedRequests. enumerated ( ) {
393+ XCTAssertEqual ( request. httpMethod, " POST " , " Request to \( request. url? . path ?? " " ) should use POST " )
394+ XCTAssertNil ( request. url? . query, " POST request URL should not contain query string " )
395+
396+ if index < capturedBodies. count {
397+ let bodyString = String ( data: capturedBodies [ index] , encoding: . utf8) ?? " "
398+ XCTAssertTrue ( bodyString. contains ( " app_key= " ) , " POST body should contain app_key for request \( index) " )
399+ }
400+ }
401+ }
402+
173403 func addRequests( count: Int ) {
174404 for loop in 0 ... count- 1 {
175405 CountlyPersistency . sharedInstance ( ) . add ( toQueue: " &request=REQUEST \( loop) " )
0 commit comments