vibetunnel/ios/VibeTunnelTests/Services/APIClientTests.swift
Igor Tarasenko 9fad6301a0
feat: Add Bonjour/mDNS service discovery for iOS app (#226)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2025-07-05 11:34:36 +01:00

345 lines
11 KiB
Swift

import Foundation
import Testing
@testable import VibeTunnel
@Suite("APIClient Tests", .tags(.critical, .networking), .disabled("Needs URL session mocking setup"))
struct APIClientTests {
let baseURL = URL(string: "http://localhost:8888")!
var mockSession: URLSession!
init() {
// Set up mock URLSession
let configuration = URLSessionConfiguration.mockConfiguration
mockSession = URLSession(configuration: configuration)
}
// MARK: - Session Management Tests
@Test("Get sessions returns parsed sessions")
@MainActor
func testGetSessions() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
#expect(request.url?.path == "/api/sessions")
#expect(request.httpMethod == "GET")
let data = TestFixtures.sessionsJSON.data(using: .utf8)!
return MockURLProtocol.successResponse(for: request.url!, data: data)
}
// Act
let client = createTestClient()
let sessions = try await client.getSessions()
// Assert
#expect(sessions.count == 2)
#expect(sessions[0].id == "test-session-123")
#expect(sessions[0].isRunning == true)
#expect(sessions[1].id == "exited-session-456")
#expect(sessions[1].isRunning == false)
}
@Test("Get sessions handles empty response")
@MainActor
func getSessionsEmpty() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
let data = "[]".data(using: .utf8)!
return MockURLProtocol.successResponse(for: request.url!, data: data)
}
// Act
let client = createTestClient()
let sessions = try await client.getSessions()
// Assert
#expect(sessions.isEmpty)
}
@Test("Get sessions handles network error", .tags(.networking))
@MainActor
func getSessionsNetworkError() async throws {
// Arrange
MockURLProtocol.requestHandler = { _ in
throw URLError(.notConnectedToInternet)
}
// Act & Assert
let client = createTestClient()
do {
_ = try await client.getSessions()
Issue.record("Expected network error")
} catch let error as APIError {
guard case .networkError = error else {
Issue.record("Expected network error, got \(error)")
return
}
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
@Test("Create session sends correct request")
@MainActor
func testCreateSession() async throws {
// Arrange
let sessionData = SessionCreateData(
command: "/bin/bash",
workingDir: "/Users/test",
name: "Test Session",
cols: 80,
rows: 24
)
MockURLProtocol.requestHandler = { request in
#expect(request.url?.path == "/api/sessions")
#expect(request.httpMethod == "POST")
#expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json")
// Verify request body
if let body = request.httpBody,
let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
{
#expect(json["command"] as? String == "/bin/bash")
#expect(json["workingDir"] as? String == "/Users/test")
#expect(json["name"] as? String == "Test Session")
#expect(json["cols"] as? Int == 80)
#expect(json["rows"] as? Int == 24)
} else {
Issue.record("Failed to parse request body")
}
let responseData = TestFixtures.createSessionJSON.data(using: .utf8)!
return MockURLProtocol.successResponse(for: request.url!, data: responseData)
}
// Act
let client = createTestClient()
let sessionId = try await client.createSession(sessionData)
// Assert
#expect(sessionId == "new-session-789")
}
@Test("Kill session sends DELETE request")
@MainActor
func testKillSession() async throws {
// Arrange
let sessionId = "test-session-123"
MockURLProtocol.requestHandler = { request in
#expect(request.url?.path == "/api/sessions/\(sessionId)")
#expect(request.httpMethod == "DELETE")
return MockURLProtocol.successResponse(for: request.url!, statusCode: 204)
}
// Act & Assert (should not throw)
let client = createTestClient()
try await client.killSession(sessionId)
}
@Test("Send input posts correct data")
@MainActor
func testSendInput() async throws {
// Arrange
let sessionId = "test-session-123"
let inputText = "ls -la\n"
MockURLProtocol.requestHandler = { request in
#expect(request.url?.path == "/api/sessions/\(sessionId)/input")
#expect(request.httpMethod == "POST")
if let body = request.httpBody,
let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
{
#expect(json["data"] as? String == inputText)
} else {
Issue.record("Failed to parse input request body")
}
return MockURLProtocol.successResponse(for: request.url!, statusCode: 204)
}
// Act & Assert (should not throw)
let client = createTestClient()
try await client.sendInput(sessionId: sessionId, text: inputText)
}
@Test("Resize terminal sends correct dimensions")
@MainActor
func testResizeTerminal() async throws {
// Arrange
let sessionId = "test-session-123"
let cols = 120
let rows = 40
MockURLProtocol.requestHandler = { request in
#expect(request.url?.path == "/api/sessions/\(sessionId)/resize")
#expect(request.httpMethod == "POST")
if let body = request.httpBody,
let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
{
#expect(json["cols"] as? Int == cols)
#expect(json["rows"] as? Int == rows)
} else {
Issue.record("Failed to parse resize request body")
}
return MockURLProtocol.successResponse(for: request.url!, statusCode: 204)
}
// Act & Assert (should not throw)
let client = createTestClient()
try await client.resizeTerminal(sessionId: sessionId, cols: cols, rows: rows)
}
// MARK: - Error Handling Tests
@Test("Handles 404 error correctly")
@MainActor
func handle404Error() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
_ = TestFixtures.errorResponseJSON.data(using: .utf8)!
return MockURLProtocol.errorResponse(
for: request.url!,
statusCode: 404,
message: "Session not found"
)
}
// Act & Assert
let client = createTestClient()
do {
_ = try await client.getSession("nonexistent")
Issue.record("Expected error to be thrown")
} catch let error as APIError {
guard case .serverError(let code, let message) = error else {
Issue.record("Expected server error, got \(error)")
return
}
#expect(code == 404)
#expect(message == "Session not found")
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
@Test("Handles 401 unauthorized error")
@MainActor
func handle401Error() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
MockURLProtocol.errorResponse(for: request.url!, statusCode: 401)
}
// Act & Assert
let client = createTestClient()
do {
_ = try await client.getSessions()
Issue.record("Expected error to be thrown")
} catch let error as APIError {
guard case .serverError(let code, _) = error else {
Issue.record("Expected server error, got \(error)")
return
}
#expect(code == 401)
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
@Test("Handles invalid JSON response")
@MainActor
func handleInvalidJSON() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
let invalidData = "not json".data(using: .utf8)!
return MockURLProtocol.successResponse(for: request.url!, data: invalidData)
}
// Act & Assert
let client = createTestClient()
do {
_ = try await client.getSessions()
Issue.record("Expected decoding error")
} catch let error as APIError {
guard case .decodingError = error else {
Issue.record("Expected decoding error, got \(error)")
return
}
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
@Test("Handles connection timeout")
@MainActor
func connectionTimeout() async throws {
// Arrange
MockURLProtocol.requestHandler = { _ in
throw URLError(.timedOut)
}
// Act & Assert
let client = createTestClient()
do {
_ = try await client.getSessions()
Issue.record("Expected network error")
} catch let error as APIError {
guard case .networkError = error else {
Issue.record("Expected network error, got \(error)")
return
}
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
// MARK: - Health Check Tests
@Test("Health check returns true for 200 response")
@MainActor
func healthCheckSuccess() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
#expect(request.url?.path == "/api/health")
return MockURLProtocol.successResponse(for: request.url!)
}
// Act
let client = createTestClient()
let isHealthy = try await client.checkHealth()
// Assert
#expect(isHealthy == true)
}
@Test("Health check returns false for error response")
@MainActor
func healthCheckFailure() async throws {
// Arrange
MockURLProtocol.requestHandler = { request in
MockURLProtocol.errorResponse(for: request.url!, statusCode: 500)
}
// Act
let client = createTestClient()
let isHealthy = try await client.checkHealth()
// Assert
#expect(isHealthy == false)
}
// MARK: - Helper Methods
@MainActor
private func createTestClient() -> APIClient {
// Create a test client with our mock session
// Note: This requires modifying APIClient to accept a custom URLSession
// For now, we'll use the shared instance and rely on MockURLProtocol
APIClient.shared
}
}