From 1617bba6a38e175f590c998671a59dab0bc4fec4 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 15 Oct 2025 15:40:00 -0300 Subject: [PATCH 1/4] fix: use IssueReporting --- Sources/Auth/AuthClient.swift | 3 ++- Sources/Realtime/RealtimeChannelV2.swift | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index bc467eed..04c3dfd6 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -1,5 +1,6 @@ import ConcurrencyExtras import Foundation +import IssueReporting #if canImport(AuthenticationServices) import AuthenticationServices @@ -1410,7 +1411,7 @@ public actor AuthClient { let session = try? await session eventEmitter.emit(.initialSession, session: session, token: token) - logger?.warning( + reportIssue( """ Initial session emitted after attempting to refresh the local stored session. This is incorrect behavior and will be fixed in the next major release since it’s a breaking change. diff --git a/Sources/Realtime/RealtimeChannelV2.swift b/Sources/Realtime/RealtimeChannelV2.swift index 0cda4efd..81543ce1 100644 --- a/Sources/Realtime/RealtimeChannelV2.swift +++ b/Sources/Realtime/RealtimeChannelV2.swift @@ -315,7 +315,8 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { body: body ) - let response = try await withTimeout(interval: timeout ?? socket.options.timeoutInterval) { [self] in + let response = try await withTimeout(interval: timeout ?? socket.options.timeoutInterval) { + [self] in await Result { try await socket.http.send(request) } @@ -346,10 +347,12 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { @MainActor public func broadcast(event: String, message: JSONObject) async { if status != .subscribed { - logger?.warning( - "Realtime broadcast() is automatically falling back to REST API. " - + "This behavior will be deprecated in the future. " - + "Please use httpSend() explicitly for REST delivery." + reportIssue( + """ + Realtime broadcast() is automatically falling back to REST API. + This behavior will be deprecated in the future. + Please use httpSend() explicitly for REST delivery. + """ ) var headers: HTTPFields = [.contentType: "application/json"] @@ -751,4 +754,3 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { push?.didReceive(status: PushStatus(rawValue: status) ?? .ok) } } - From d91c4cf8736de79817434559590aa6e7fe1c5521 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 15 Oct 2025 15:58:30 -0300 Subject: [PATCH 2/4] drop test --- Tests/RealtimeTests/RealtimeTests.swift | 49 +------------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index f24aec6f..db86c78a 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -241,7 +241,7 @@ final class RealtimeTests: XCTestCase { // Wait for the timeout for rejoining. await testClock.advance(by: .seconds(timeoutInterval)) - + // Wait for the retry delay (base delay is 1.0s, but we need to account for jitter) // The retry delay is calculated as: baseDelay * pow(2, attempt-1) + jitter // For attempt 2: 1.0 * pow(2, 1) = 2.0s + jitter (up to ±25% = ±0.5s) @@ -443,7 +443,7 @@ final class RealtimeTests: XCTestCase { await testClock.advance(by: .seconds(timeoutInterval)) subscribeTask.cancel() - + do { try await subscribeTask.value XCTFail("Expected cancellation error but got success") @@ -575,51 +575,6 @@ final class RealtimeTests: XCTestCase { XCTAssertEqual(heartbeatStatuses.value, [.sent, .timeout]) } - func testBroadcastWithHTTP() async throws { - await http.when { - $0.url.path.hasSuffix("broadcast") - } return: { _ in - HTTPResponse( - data: "{}".data(using: .utf8)!, - response: HTTPURLResponse( - url: self.sut.broadcastURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! - ) - } - - let channel = sut.channel("public:messages") { - $0.broadcast.acknowledgeBroadcasts = true - } - - try await channel.broadcast(event: "test", message: ["value": 42]) - - let request = await http.receivedRequests.last - assertInlineSnapshot(of: request?.urlRequest, as: .raw(pretty: true)) { - """ - POST http://localhost:54321/realtime/v1/api/broadcast - Authorization: Bearer custom.access.token - Content-Type: application/json - apiKey: anon.api.key - - { - "messages" : [ - { - "event" : "test", - "payload" : { - "value" : 42 - }, - "private" : false, - "topic" : "realtime:public:messages" - } - ] - } - """ - } - } - func testSetAuth() async { let validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjY0MDkyMjExMjAwfQ.GfiEKLl36X8YWcatHg31jRbilovlGecfUKnOyXMSX9c" From c5d15e33e01006c18fa17035db11c3b76f2e2323 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 16 Oct 2025 06:10:02 -0300 Subject: [PATCH 3/4] fix: disable reportIssue in tests --- Sources/Auth/AuthClient.swift | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 04c3dfd6..5d8f8024 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -1411,17 +1411,21 @@ public actor AuthClient { let session = try? await session eventEmitter.emit(.initialSession, session: session, token: token) - reportIssue( - """ - Initial session emitted after attempting to refresh the local stored session. - This is incorrect behavior and will be fixed in the next major release since it’s a breaking change. - For now, if you want to opt-in to the new behavior, add the trait `EmitLocalSessionAsInitialSession` to your Package.swift file when importing the Supabase dependency. - The new behavior ensures that the locally stored session is always emitted, regardless of its validity or expiration. - If you rely on the initial session to opt users in, you need to add an additional check for `session.isExpired` in the session. - - Check https://github.com/supabase/supabase-swift/pull/822 for more information. - """ - ) + // Properly expecting issues during tests isn't working as expected, I think because the reportIssue is usually triggered inside an unstructured Task + // because of this I'm disabling issue reporting during tests, so we can use it only for advising developers when running their applications. + if !isTesting { + reportIssue( + """ + Initial session emitted after attempting to refresh the local stored session. + This is incorrect behavior and will be fixed in the next major release since it’s a breaking change. + For now, if you want to opt-in to the new behavior, add the trait `EmitLocalSessionAsInitialSession` to your Package.swift file when importing the Supabase dependency. + The new behavior ensures that the locally stored session is always emitted, regardless of its validity or expiration. + If you rely on the initial session to opt users in, you need to add an additional check for `session.isExpired` in the session. + + Check https://github.com/supabase/supabase-swift/pull/822 for more information. + """ + ) + } #endif } From 6f14be965e70c1c0d6d499117300374acd8edc28 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 16 Oct 2025 06:35:54 -0300 Subject: [PATCH 4/4] test: revert broadcast with HTTP test --- Sources/Helpers/Codable.swift | 11 +++++++- Sources/Realtime/RealtimeChannelV2.swift | 22 +++++++++------ Tests/RealtimeTests/RealtimeTests.swift | 35 ++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/Sources/Helpers/Codable.swift b/Sources/Helpers/Codable.swift index e6b38877..1efaa6aa 100644 --- a/Sources/Helpers/Codable.swift +++ b/Sources/Helpers/Codable.swift @@ -7,6 +7,7 @@ import ConcurrencyExtras import Foundation +import XCTestDynamicOverlay extension JSONDecoder { /// Default `JSONDecoder` for decoding types from Supabase. @@ -21,7 +22,8 @@ extension JSONDecoder { } throw DecodingError.dataCorruptedError( - in: container, debugDescription: "Invalid date format: \(string)" + in: container, + debugDescription: "Invalid date format: \(string)" ) } return decoder @@ -36,6 +38,13 @@ extension JSONEncoder { let string = date.iso8601String try container.encode(string) } + + #if DEBUG + if isTesting { + encoder.outputFormatting = [.sortedKeys] + } + #endif + return encoder } } diff --git a/Sources/Realtime/RealtimeChannelV2.swift b/Sources/Realtime/RealtimeChannelV2.swift index 81543ce1..2be951d9 100644 --- a/Sources/Realtime/RealtimeChannelV2.swift +++ b/Sources/Realtime/RealtimeChannelV2.swift @@ -295,7 +295,7 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { } headers[.authorization] = "Bearer \(accessToken)" - let body = try await JSONEncoder().encode( + let body = try await JSONEncoder.supabase().encode( BroadcastMessagePayload( messages: [ BroadcastMessagePayload.Message( @@ -347,13 +347,17 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { @MainActor public func broadcast(event: String, message: JSONObject) async { if status != .subscribed { - reportIssue( - """ - Realtime broadcast() is automatically falling back to REST API. - This behavior will be deprecated in the future. - Please use httpSend() explicitly for REST delivery. - """ - ) + // Properly expecting issues during tests isn't working as expected, I think because the reportIssue is usually triggered inside an unstructured Task + // because of this I'm disabling issue reporting during tests, so we can use it only for advising developers when running their applications. + if !isTesting { + reportIssue( + """ + Realtime broadcast() is automatically falling back to REST API. + This behavior will be deprecated in the future. + Please use httpSend() explicitly for REST delivery. + """ + ) + } var headers: HTTPFields = [.contentType: "application/json"] if let apiKey = socket.options.apikey { @@ -369,7 +373,7 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { url: socket.broadcastURL, method: .post, headers: headers, - body: JSONEncoder().encode( + body: JSONEncoder.supabase().encode( BroadcastMessagePayload( messages: [ BroadcastMessagePayload.Message( diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index db86c78a..5febd126 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -575,6 +575,41 @@ final class RealtimeTests: XCTestCase { XCTAssertEqual(heartbeatStatuses.value, [.sent, .timeout]) } + func testBroadcastWithHTTP() async throws { + await http.when { + $0.url.path.hasSuffix("broadcast") + } return: { _ in + HTTPResponse( + data: "{}".data(using: .utf8)!, + response: HTTPURLResponse( + url: self.sut.broadcastURL, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + ) + } + + let channel = sut.channel("public:messages") { + $0.broadcast.acknowledgeBroadcasts = true + } + + try await channel.broadcast(event: "test", message: ["value": 42]) + + let request = await http.receivedRequests.last + assertInlineSnapshot(of: request?.urlRequest, as: .curl) { + #""" + curl \ + --request POST \ + --header "Authorization: Bearer custom.access.token" \ + --header "Content-Type: application/json" \ + --header "apiKey: anon.api.key" \ + --data "{\"messages\":[{\"event\":\"test\",\"payload\":{\"value\":42},\"private\":false,\"topic\":\"realtime:public:messages\"}]}" \ + "http://localhost:54321/realtime/v1/api/broadcast" + """# + } + } + func testSetAuth() async { let validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjY0MDkyMjExMjAwfQ.GfiEKLl36X8YWcatHg31jRbilovlGecfUKnOyXMSX9c"