vibetunnel/VibeTunnelTests/BasicAuthMiddlewareTests.swift
2025-06-19 01:39:27 +02:00

397 lines
No EOL
14 KiB
Swift

import Testing
import Foundation
import HTTPTypes
import Hummingbird
import HummingbirdCore
import NIOCore
import Logging
@testable import VibeTunnel
// MARK: - Mock Request Context
// For testing, we'll use the BasicRequestContext with a test application
import NIOEmbedded
struct TestRequestContext {
static func create() -> BasicRequestContext {
// Create a test channel and logger for the context source
let channel = EmbeddedChannel()
let logger = Logger(label: "test")
let source = ApplicationRequestContextSource(channel: channel, logger: logger)
return BasicRequestContext(source: source)
}
}
// MARK: - Test Helpers
extension String {
/// Encode string as Base64 for Basic Auth
var base64Encoded: String {
Data(self.utf8).base64EncodedString()
}
}
// MARK: - Basic Auth Middleware Tests
@Suite("Basic Auth Middleware Tests", .tags(.security, .networking))
struct BasicAuthMiddlewareTests {
// Helper to create a test request
func createRequest(
path: String = "/",
method: HTTPRequest.Method = .get,
headers: HTTPFields = HTTPFields()
) -> Request {
Request(
head: HTTPRequest(
method: method,
scheme: "http",
authority: "localhost",
path: path,
headerFields: headers
),
body: RequestBody(buffer: ByteBuffer())
)
}
// Helper to create a mock next handler
func createNextHandler() -> (Request, BasicRequestContext) async throws -> Response {
return { request, context in
Response(status: .ok)
}
}
// MARK: - Valid Authentication Tests
@Test("Valid authentication", arguments: zip(
["user:pass", "admin:secret", "test:password123"],
["pass", "secret", "password123"]
))
func testValidAuth(credentials: String, expectedPassword: String) async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: expectedPassword)
var headers = HTTPFields()
headers[.authorization] = "Basic \(credentials.base64Encoded)"
let request = createRequest(headers: headers)
let context = TestRequestContext.create()
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .ok)
}
@Test("Valid auth with special characters", arguments: [
"user:p@ssw0rd!",
"admin:test-password-123",
"test:password with spaces",
"user:пароль", // Cyrillic
"admin:パスワード", // Japanese
"test:🔐secure🔐" // Emoji
])
func testValidAuthSpecialChars(credentials: String) async throws {
let parts = credentials.split(separator: ":", maxSplits: 1)
let password = String(parts[1])
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: password)
var headers = HTTPFields()
headers[.authorization] = "Basic \(credentials.base64Encoded)"
let request = createRequest(headers: headers)
let context = TestRequestContext.create()
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .ok)
}
// MARK: - Invalid Authentication Tests
@Test("Invalid authentication attempts")
func testInvalidAuth() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "correct-password")
let context = TestRequestContext.create()
// Wrong password
var headers = HTTPFields()
headers[.authorization] = "Basic \("user:wrong-password".base64Encoded)"
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .unauthorized)
#expect(response.headers[HTTPField.Name("WWW-Authenticate")!]?.contains("Basic realm=") == true)
}
@Test("Missing authorization header")
func testMissingAuthHeader() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
let request = createRequest() // No auth header
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .unauthorized)
#expect(response.headers[HTTPField.Name("WWW-Authenticate")!] == "Basic realm=\"VibeTunnel Dashboard\"")
}
@Test("Invalid authorization header format", arguments: [
"Bearer token123", // Wrong auth type
"Basic", // Missing credentials
"Basic ", // Empty credentials
"InvalidHeader", // Completely wrong format
"basic dXNlcjpwYXNz" // Lowercase 'basic'
])
func testInvalidAuthHeaderFormat(authHeader: String) async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
var headers = HTTPFields()
headers[.authorization] = authHeader
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .unauthorized)
}
@Test("Invalid base64 encoding")
func testInvalidBase64() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
var headers = HTTPFields()
headers[.authorization] = "Basic !!!invalid-base64!!!"
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .unauthorized)
}
@Test("Missing colon in credentials")
func testMissingColon() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
var headers = HTTPFields()
headers[.authorization] = "Basic \("userpassword".base64Encoded)" // No colon separator
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .unauthorized)
}
// MARK: - Health Check Bypass Tests
@Test("Health check endpoint bypasses auth")
func testHealthCheckBypass() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
// Request to health endpoint without auth
let request = createRequest(path: "/api/health")
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .ok) // Should pass without auth
}
@Test("Other endpoints require auth", arguments: [
"/",
"/api/sessions",
"/api/cleanup",
"/dashboard",
"/api/health/detailed" // Similar but different path
])
func testOtherEndpointsRequireAuth(path: String) async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
// Request without auth
let request = createRequest(path: path)
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .unauthorized)
}
// MARK: - Custom Realm Tests
@Test("Custom realm configuration")
func testCustomRealm() async throws {
let customRealm = "My Custom Realm"
let middleware = BasicAuthMiddleware<BasicRequestContext>(
password: "password",
realm: customRealm
)
let context = TestRequestContext.create()
let request = createRequest() // No auth
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .unauthorized)
#expect(response.headers[HTTPField.Name("WWW-Authenticate")!] == "Basic realm=\"\(customRealm)\"")
}
// MARK: - Rate Limiting Tests
@Test("Rate limiting", .tags(.security))
func testRateLimiting() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "correct-password")
let context = TestRequestContext.create()
// Multiple failed attempts
var headers = HTTPFields()
headers[.authorization] = "Basic \("user:wrong".base64Encoded)"
// Make multiple requests
for _ in 0..<5 {
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .unauthorized)
}
// Note: Current implementation doesn't have rate limiting
// This test documents expected behavior for future implementation
}
// MARK: - Username Handling Tests
@Test("Username is ignored", arguments: [
"admin:password",
"user:password",
"any-username:password",
":password" // Empty username
])
func testUsernameIgnored(credentials: String) async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
var headers = HTTPFields()
headers[.authorization] = "Basic \(credentials.base64Encoded)"
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .ok)
}
// MARK: - Response Body Tests
@Test("Unauthorized response includes message")
func testUnauthorizedResponseBody() async throws {
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "password")
let context = TestRequestContext.create()
let request = createRequest() // No auth
let response = try await middleware.handle(request, context: context, next: createNextHandler())
#expect(response.status == .unauthorized)
// For now, skip body check due to API differences
// TODO: Fix body checking once ResponseBody API is clarified
}
// MARK: - Security Edge Cases
@Test("Empty password handling")
func testEmptyPassword() async throws {
// Middleware with empty password (should probably be prevented in real usage)
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: "")
let context = TestRequestContext.create()
var headers = HTTPFields()
headers[.authorization] = "Basic \("user:".base64Encoded)" // Empty password in request
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .ok) // Matches empty password
}
@Test("Very long credentials")
func testVeryLongCredentials() async throws {
let longPassword = String(repeating: "a", count: 1000)
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: longPassword)
let context = TestRequestContext.create()
var headers = HTTPFields()
headers[.authorization] = "Basic \("user:\(longPassword)".base64Encoded)"
let response = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(response.status == .ok)
}
// MARK: - Integration Tests
@Test("Full authentication flow", .tags(.integration))
func testFullAuthFlow() async throws {
let password = "secure-dashboard-password"
let middleware = BasicAuthMiddleware<BasicRequestContext>(password: password)
let context = TestRequestContext.create()
// 1. No auth - should fail
let noAuthResponse = try await middleware.handle(
createRequest(),
context: context,
next: createNextHandler()
)
#expect(noAuthResponse.status == .unauthorized)
// 2. Wrong password - should fail
var headers = HTTPFields()
headers[.authorization] = "Basic \("admin:wrong".base64Encoded)"
let wrongAuthResponse = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(wrongAuthResponse.status == .unauthorized)
// 3. Correct password - should succeed
headers[.authorization] = "Basic \("admin:\(password)".base64Encoded)"
let correctAuthResponse = try await middleware.handle(
createRequest(headers: headers),
context: context,
next: createNextHandler()
)
#expect(correctAuthResponse.status == .ok)
// 4. Health check - should succeed without auth
let healthResponse = try await middleware.handle(
createRequest(path: "/api/health"),
context: context,
next: createNextHandler()
)
#expect(healthResponse.status == .ok)
}
}