mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix bind address reverting to localhost after server restart (#404)
This commit is contained in:
parent
453d888731
commit
44946f1006
13 changed files with 234 additions and 42 deletions
|
|
@ -42,6 +42,12 @@ enum AppConstants {
|
||||||
static let newSessionTitleMode = "NewSession.titleMode"
|
static let newSessionTitleMode = "NewSession.titleMode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw string values for DashboardAccessMode
|
||||||
|
enum DashboardAccessModeRawValues {
|
||||||
|
static let localhost = "localhost"
|
||||||
|
static let network = "network"
|
||||||
|
}
|
||||||
|
|
||||||
/// Default values for UserDefaults
|
/// Default values for UserDefaults
|
||||||
enum Defaults {
|
enum Defaults {
|
||||||
/// Sleep prevention is enabled by default for better user experience
|
/// Sleep prevention is enabled by default for better user experience
|
||||||
|
|
@ -53,7 +59,7 @@ enum AppConstants {
|
||||||
|
|
||||||
// Server Configuration
|
// Server Configuration
|
||||||
static let serverPort = 4_020
|
static let serverPort = 4_020
|
||||||
static let dashboardAccessMode = "localhost"
|
static let dashboardAccessMode = DashboardAccessModeRawValues.network
|
||||||
static let cleanupOnStartup = true
|
static let cleanupOnStartup = true
|
||||||
static let authenticationMode = "os"
|
static let authenticationMode = "os"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import Foundation
|
||||||
/// Controls whether the web interface is accessible only locally or
|
/// Controls whether the web interface is accessible only locally or
|
||||||
/// from other devices on the network.
|
/// from other devices on the network.
|
||||||
enum DashboardAccessMode: String, CaseIterable {
|
enum DashboardAccessMode: String, CaseIterable {
|
||||||
|
// Raw values are automatically inferred as "localhost" and "network"
|
||||||
|
// These must match AppConstants.DashboardAccessModeRawValues
|
||||||
case localhost
|
case localhost
|
||||||
case network
|
case network
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,24 @@ class ServerManager {
|
||||||
|
|
||||||
var bindAddress: String {
|
var bindAddress: String {
|
||||||
get {
|
get {
|
||||||
let mode = DashboardAccessMode(
|
// Get the raw value from UserDefaults, defaulting to the app default
|
||||||
rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
|
let rawValue = UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? AppConstants.Defaults
|
||||||
) ??
|
.dashboardAccessMode
|
||||||
.localhost
|
let mode = DashboardAccessMode(rawValue: rawValue) ?? .network
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
logger
|
||||||
|
.debug(
|
||||||
|
"bindAddress getter: rawValue='\(rawValue)', mode=\(mode.rawValue), bindAddress=\(mode.bindAddress)"
|
||||||
|
)
|
||||||
|
|
||||||
return mode.bindAddress
|
return mode.bindAddress
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
// Find the mode that matches this bind address
|
// Find the mode that matches this bind address
|
||||||
if let mode = DashboardAccessMode.allCases.first(where: { $0.bindAddress == newValue }) {
|
if let mode = DashboardAccessMode.allCases.first(where: { $0.bindAddress == newValue }) {
|
||||||
UserDefaults.standard.set(mode.rawValue, forKey: "dashboardAccessMode")
|
UserDefaults.standard.set(mode.rawValue, forKey: "dashboardAccessMode")
|
||||||
|
logger.debug("bindAddress setter: set mode=\(mode.rawValue) for bindAddress=\(newValue)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +217,9 @@ class ServerManager {
|
||||||
do {
|
do {
|
||||||
let server = BunServer()
|
let server = BunServer()
|
||||||
server.port = port
|
server.port = port
|
||||||
server.bindAddress = bindAddress
|
let currentBindAddress = bindAddress
|
||||||
|
server.bindAddress = currentBindAddress
|
||||||
|
logger.info("Starting server with port=\(self.port), bindAddress=\(currentBindAddress)")
|
||||||
|
|
||||||
// Set up crash handler
|
// Set up crash handler
|
||||||
server.onCrash = { [weak self] exitCode in
|
server.onCrash = { [weak self] exitCode in
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ struct DashboardSettingsView: View {
|
||||||
@AppStorage(AppConstants.UserDefaultsKeys.serverPort)
|
@AppStorage(AppConstants.UserDefaultsKeys.serverPort)
|
||||||
private var serverPort = "4020"
|
private var serverPort = "4020"
|
||||||
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
private var accessModeString = DashboardAccessMode.network.rawValue
|
private var accessModeString = AppConstants.Defaults.dashboardAccessMode
|
||||||
|
|
||||||
@Environment(ServerManager.self)
|
@Environment(ServerManager.self)
|
||||||
private var serverManager
|
private var serverManager
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ struct GeneralSettingsView: View {
|
||||||
@AppStorage(AppConstants.UserDefaultsKeys.serverPort)
|
@AppStorage(AppConstants.UserDefaultsKeys.serverPort)
|
||||||
private var serverPort = "4020"
|
private var serverPort = "4020"
|
||||||
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
private var accessModeString = DashboardAccessMode.network.rawValue
|
private var accessModeString = AppConstants.Defaults.dashboardAccessMode
|
||||||
|
|
||||||
@State private var isCheckingForUpdates = false
|
@State private var isCheckingForUpdates = false
|
||||||
@State private var localIPAddress: String?
|
@State private var localIPAddress: String?
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ struct RemoteAccessSettingsView: View {
|
||||||
@AppStorage(AppConstants.UserDefaultsKeys.serverPort)
|
@AppStorage(AppConstants.UserDefaultsKeys.serverPort)
|
||||||
private var serverPort = "4020"
|
private var serverPort = "4020"
|
||||||
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
private var accessModeString = DashboardAccessMode.network.rawValue
|
private var accessModeString = AppConstants.Defaults.dashboardAccessMode
|
||||||
|
|
||||||
@Environment(NgrokService.self)
|
@Environment(NgrokService.self)
|
||||||
private var ngrokService
|
private var ngrokService
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import SwiftUI
|
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/// Project folder configuration page in the welcome flow.
|
/// Project folder configuration page in the welcome flow.
|
||||||
///
|
///
|
||||||
|
|
@ -23,7 +23,7 @@ struct ProjectFolderPageView: View {
|
||||||
@State private var scanTask: Task<Void, Never>?
|
@State private var scanTask: Task<Void, Never>?
|
||||||
@Binding var currentPage: Int
|
@Binding var currentPage: Int
|
||||||
|
|
||||||
// Page index for ProjectFolderPageView in the welcome flow
|
/// Page index for ProjectFolderPageView in the welcome flow
|
||||||
private let pageIndex = 4
|
private let pageIndex = 4
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -239,7 +239,9 @@ struct ProjectFolderPageView: View {
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
// Task was cancelled, stop scanning
|
// Task was cancelled, stop scanning
|
||||||
return
|
return
|
||||||
} catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoPermissionError {
|
} catch let error as NSError
|
||||||
|
where error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoPermissionError
|
||||||
|
{
|
||||||
// Silently ignore permission errors - common for system directories
|
// Silently ignore permission errors - common for system directories
|
||||||
} catch let error as NSError where error.domain == NSPOSIXErrorDomain && error.code == 1 {
|
} catch let error as NSError where error.domain == NSPOSIXErrorDomain && error.code == 1 {
|
||||||
// Operation not permitted - another common permission error
|
// Operation not permitted - another common permission error
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,10 @@ final class CLIInstaller {
|
||||||
// Verify it's our wrapper script with all expected components
|
// Verify it's our wrapper script with all expected components
|
||||||
// Check for the exec command with flexible quoting and optional arguments
|
// Check for the exec command with flexible quoting and optional arguments
|
||||||
// Allow for optional variables or arguments between $VIBETUNNEL_BIN and fwd
|
// Allow for optional variables or arguments between $VIBETUNNEL_BIN and fwd
|
||||||
let hasValidExecCommand = content.range(of: #"exec\s+["']?\$VIBETUNNEL_BIN["']?\s+fwd"#, options: .regularExpression) != nil
|
let hasValidExecCommand = content.range(
|
||||||
|
of: #"exec\s+["']?\$VIBETUNNEL_BIN["']?\s+fwd"#,
|
||||||
|
options: .regularExpression
|
||||||
|
) != nil
|
||||||
|
|
||||||
if content.contains("VibeTunnel CLI wrapper") &&
|
if content.contains("VibeTunnel CLI wrapper") &&
|
||||||
content.contains("$TRY_PATH/Contents/Resources/vibetunnel") &&
|
content.contains("$TRY_PATH/Contents/Resources/vibetunnel") &&
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,8 @@ struct VibeTunnelApp: App {
|
||||||
/// coordinator for application-wide events and services.
|
/// coordinator for application-wide events and services.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
|
||||||
// Needed for some gross menu item highlight hack
|
// Needed for menu item highlight hack
|
||||||
weak static var shared: AppDelegate?
|
static weak var shared: AppDelegate?
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
Self.shared = self
|
Self.shared = self
|
||||||
|
|
@ -180,7 +180,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
|
||||||
|
|
||||||
// Register default values
|
// Register default values
|
||||||
UserDefaults.standard.register(defaults: [
|
UserDefaults.standard.register(defaults: [
|
||||||
"showInDock": true // Default to showing in dock
|
"showInDock": true, // Default to showing in dock
|
||||||
|
"dashboardAccessMode": AppConstants.Defaults.dashboardAccessMode
|
||||||
])
|
])
|
||||||
|
|
||||||
// Initialize Sparkle updater manager
|
// Initialize Sparkle updater manager
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,8 @@ struct ModelTests {
|
||||||
|
|
||||||
@Test("DashboardAccessMode raw values")
|
@Test("DashboardAccessMode raw values")
|
||||||
func rawValues() throws {
|
func rawValues() throws {
|
||||||
#expect(DashboardAccessMode.localhost.rawValue == "localhost")
|
#expect(DashboardAccessMode.localhost.rawValue == AppConstants.DashboardAccessModeRawValues.localhost)
|
||||||
#expect(DashboardAccessMode.network.rawValue == "network")
|
#expect(DashboardAccessMode.network.rawValue == AppConstants.DashboardAccessModeRawValues.network)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("DashboardAccessMode descriptions")
|
@Test("DashboardAccessMode descriptions")
|
||||||
|
|
@ -147,6 +147,26 @@ struct ModelTests {
|
||||||
#expect(DashboardAccessMode.localhost.description.contains("this Mac"))
|
#expect(DashboardAccessMode.localhost.description.contains("this Mac"))
|
||||||
#expect(DashboardAccessMode.network.description.contains("other devices"))
|
#expect(DashboardAccessMode.network.description.contains("other devices"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("DashboardAccessMode default value")
|
||||||
|
func defaultValue() throws {
|
||||||
|
// Verify the default is network mode
|
||||||
|
#expect(AppConstants.Defaults.dashboardAccessMode == DashboardAccessMode.network.rawValue)
|
||||||
|
|
||||||
|
// Verify we can create a mode from the default
|
||||||
|
let mode = DashboardAccessMode(rawValue: AppConstants.Defaults.dashboardAccessMode)
|
||||||
|
#expect(mode == .network)
|
||||||
|
#expect(mode?.bindAddress == "0.0.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("DashboardAccessMode from invalid raw value")
|
||||||
|
func invalidRawValue() throws {
|
||||||
|
let mode = DashboardAccessMode(rawValue: "invalid")
|
||||||
|
#expect(mode == nil)
|
||||||
|
|
||||||
|
let emptyMode = DashboardAccessMode(rawValue: "")
|
||||||
|
#expect(emptyMode == nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UpdateChannel Tests
|
// MARK: - UpdateChannel Tests
|
||||||
|
|
@ -246,6 +266,58 @@ struct ModelTests {
|
||||||
@Test("UserDefaults keys")
|
@Test("UserDefaults keys")
|
||||||
func userDefaultsKeys() throws {
|
func userDefaultsKeys() throws {
|
||||||
#expect(AppConstants.UserDefaultsKeys.welcomeVersion == "welcomeVersion")
|
#expect(AppConstants.UserDefaultsKeys.welcomeVersion == "welcomeVersion")
|
||||||
|
#expect(AppConstants.UserDefaultsKeys.dashboardAccessMode == "dashboardAccessMode")
|
||||||
|
#expect(AppConstants.UserDefaultsKeys.serverPort == "serverPort")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("AppConstants default values")
|
||||||
|
func defaultValues() throws {
|
||||||
|
// Verify dashboard access mode default
|
||||||
|
#expect(AppConstants.Defaults.dashboardAccessMode == DashboardAccessMode.network.rawValue)
|
||||||
|
|
||||||
|
// Verify server port default
|
||||||
|
#expect(AppConstants.Defaults.serverPort == 4_020)
|
||||||
|
|
||||||
|
// Verify other defaults
|
||||||
|
#expect(AppConstants.Defaults.cleanupOnStartup == true)
|
||||||
|
#expect(AppConstants.Defaults.showInDock == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("AppConstants stringValue helper with dashboardAccessMode")
|
||||||
|
func stringValueHelper() throws {
|
||||||
|
// Store original value
|
||||||
|
let originalValue = UserDefaults.standard.string(forKey: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
|
||||||
|
defer {
|
||||||
|
// Restore original value
|
||||||
|
if let originalValue {
|
||||||
|
UserDefaults.standard.set(originalValue, forKey: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
} else {
|
||||||
|
UserDefaults.standard.removeObject(forKey: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When key doesn't exist, should return default
|
||||||
|
UserDefaults.standard.removeObject(forKey: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
let defaultValue = AppConstants.stringValue(for: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
#expect(defaultValue == AppConstants.Defaults.dashboardAccessMode)
|
||||||
|
#expect(defaultValue == AppConstants.DashboardAccessModeRawValues.network) // Our default is network
|
||||||
|
|
||||||
|
// When key exists with localhost, should return localhost
|
||||||
|
UserDefaults.standard.set(
|
||||||
|
AppConstants.DashboardAccessModeRawValues.localhost,
|
||||||
|
forKey: AppConstants.UserDefaultsKeys.dashboardAccessMode
|
||||||
|
)
|
||||||
|
let localhostValue = AppConstants.stringValue(for: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
#expect(localhostValue == AppConstants.DashboardAccessModeRawValues.localhost)
|
||||||
|
|
||||||
|
// When key exists with network, should return network
|
||||||
|
UserDefaults.standard.set(
|
||||||
|
AppConstants.DashboardAccessModeRawValues.network,
|
||||||
|
forKey: AppConstants.UserDefaultsKeys.dashboardAccessMode
|
||||||
|
)
|
||||||
|
let networkValue = AppConstants.stringValue(for: AppConstants.UserDefaultsKeys.dashboardAccessMode)
|
||||||
|
#expect(networkValue == AppConstants.DashboardAccessModeRawValues.network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,101 @@ final class ServerManagerTests {
|
||||||
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Bind address default value")
|
||||||
|
func bindAddressDefaultValue() async throws {
|
||||||
|
// Store original value
|
||||||
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode")
|
||||||
|
|
||||||
|
// Remove the key to test default behavior
|
||||||
|
UserDefaults.standard.removeObject(forKey: "dashboardAccessMode")
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
|
||||||
|
// Should default to network mode (0.0.0.0)
|
||||||
|
#expect(manager.bindAddress == "0.0.0.0")
|
||||||
|
|
||||||
|
// Restore original value
|
||||||
|
if let originalMode {
|
||||||
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Bind address setter")
|
||||||
|
func bindAddressSetter() async throws {
|
||||||
|
// Store original value
|
||||||
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode")
|
||||||
|
|
||||||
|
// Test setting via bind address
|
||||||
|
manager.bindAddress = "127.0.0.1"
|
||||||
|
#expect(UserDefaults.standard.string(forKey: "dashboardAccessMode") == AppConstants.DashboardAccessModeRawValues
|
||||||
|
.localhost
|
||||||
|
)
|
||||||
|
#expect(manager.bindAddress == "127.0.0.1")
|
||||||
|
|
||||||
|
manager.bindAddress = "0.0.0.0"
|
||||||
|
#expect(UserDefaults.standard.string(forKey: "dashboardAccessMode") == AppConstants.DashboardAccessModeRawValues
|
||||||
|
.network
|
||||||
|
)
|
||||||
|
#expect(manager.bindAddress == "0.0.0.0")
|
||||||
|
|
||||||
|
// Test invalid bind address (should not change UserDefaults)
|
||||||
|
manager.bindAddress = "192.168.1.1"
|
||||||
|
#expect(manager.bindAddress == "0.0.0.0") // Should still be the last valid value
|
||||||
|
|
||||||
|
// Restore original value
|
||||||
|
if let originalMode {
|
||||||
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
||||||
|
} else {
|
||||||
|
UserDefaults.standard.removeObject(forKey: "dashboardAccessMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Bind address persistence across server restarts")
|
||||||
|
func bindAddressPersistence() async throws {
|
||||||
|
// Store original values
|
||||||
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode")
|
||||||
|
let originalPort = manager.port
|
||||||
|
|
||||||
|
// Set to localhost mode
|
||||||
|
UserDefaults.standard.set(AppConstants.DashboardAccessModeRawValues.localhost, forKey: "dashboardAccessMode")
|
||||||
|
manager.port = "4021"
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
await manager.start()
|
||||||
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
|
||||||
|
// Verify bind address
|
||||||
|
#expect(manager.bindAddress == "127.0.0.1")
|
||||||
|
|
||||||
|
// Restart server
|
||||||
|
await manager.restart()
|
||||||
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
|
||||||
|
// Bind address should persist
|
||||||
|
#expect(manager.bindAddress == "127.0.0.1")
|
||||||
|
#expect(UserDefaults.standard.string(forKey: "dashboardAccessMode") == AppConstants.DashboardAccessModeRawValues
|
||||||
|
.localhost
|
||||||
|
)
|
||||||
|
|
||||||
|
// Change to network mode
|
||||||
|
UserDefaults.standard.set(AppConstants.DashboardAccessModeRawValues.network, forKey: "dashboardAccessMode")
|
||||||
|
|
||||||
|
// Restart again
|
||||||
|
await manager.restart()
|
||||||
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
|
||||||
|
// Should now be network mode
|
||||||
|
#expect(manager.bindAddress == "0.0.0.0")
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await manager.stop()
|
||||||
|
manager.port = originalPort
|
||||||
|
if let originalMode {
|
||||||
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
||||||
|
} else {
|
||||||
|
UserDefaults.standard.removeObject(forKey: "dashboardAccessMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Concurrent Operations Tests
|
// MARK: - Concurrent Operations Tests
|
||||||
|
|
||||||
@Test("Concurrent server operations are serialized", .tags(.concurrency))
|
@Test("Concurrent server operations are serialized", .tags(.concurrency))
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,8 @@ struct SystemControlHandlerTests {
|
||||||
|
|
||||||
if let responseData = response,
|
if let responseData = response,
|
||||||
let responseJson = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any],
|
let responseJson = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any],
|
||||||
let payload = responseJson["payload"] as? [String: Any] {
|
let payload = responseJson["payload"] as? [String: Any]
|
||||||
|
{
|
||||||
// The handler should return success but the actual UserDefaults update
|
// The handler should return success but the actual UserDefaults update
|
||||||
// should only happen for source="web"
|
// should only happen for source="web"
|
||||||
#expect(payload["success"] as? Bool == true)
|
#expect(payload["success"] as? Bool == true)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue