// // CLIServerServiceTests.swift // oAITests // // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Copyright (C) 2026 Rune Olsen import Testing import Foundation @testable import Confab @Suite("CLIServerService pure HTTP helpers") struct CLIServerServiceTests { // MARK: - parseRequestBody @Test("Returns the body once headers and full Content-Length body have arrived") func parsesCompleteRequest() { let body = "{\"prompt\":\"hi\"}" let raw = "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: \(body.utf8.count)\r\n\r\n\(body)" let result = CLIServerService.parseRequestBody(from: Data(raw.utf8)) #expect(result.map { String(data: $0, encoding: .utf8) } == body) } @Test("Returns nil when the header block hasn't fully arrived yet") func returnsNilForIncompleteHeaders() { let raw = "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5" #expect(CLIServerService.parseRequestBody(from: Data(raw.utf8)) == nil) } @Test("Returns nil when the body hasn't fully arrived yet") func returnsNilForIncompleteBody() { let raw = "POST / HTTP/1.1\r\nContent-Length: 20\r\n\r\n{\"prompt\":\"hi\"}" #expect(CLIServerService.parseRequestBody(from: Data(raw.utf8)) == nil) } @Test("Content-Length header name match is case-insensitive") func headerNameIsCaseInsensitive() { let body = "abc" let raw = "POST / HTTP/1.1\r\ncontent-length: 3\r\n\r\n\(body)" let result = CLIServerService.parseRequestBody(from: Data(raw.utf8)) #expect(result.map { String(data: $0, encoding: .utf8) } == body) } @Test("Missing Content-Length is treated as a zero-length body") func missingContentLengthIsEmptyBody() { let raw = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" let result = CLIServerService.parseRequestBody(from: Data(raw.utf8)) #expect(result?.isEmpty == true) } @Test("Extra trailing bytes beyond Content-Length don't prevent extraction (pipelined data ignored)") func extraTrailingBytesStillExtractsBody() { let body = "abc" let raw = "POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\n\(body)EXTRA" let result = CLIServerService.parseRequestBody(from: Data(raw.utf8)) #expect(result.map { String(data: $0, encoding: .utf8) } == body) } // MARK: - Response encoding @Test("successResponse produces a 200 with the response field set") func successResponseShape() throws { let data = CLIServerService.successResponse("hello world") let text = String(data: data, encoding: .utf8)! #expect(text.hasPrefix("HTTP/1.1 200 OK\r\n")) #expect(text.contains("Content-Type: application/json")) let bodyStart = text.range(of: "\r\n\r\n")!.upperBound let bodyJSON = Data(text[bodyStart...].utf8) let decoded = try JSONDecoder().decode(CLIServerService.AskResponseBody.self, from: bodyJSON) #expect(decoded.response == "hello world") #expect(decoded.error == nil) } @Test("errorResponse defaults to 400 Bad Request") func errorResponseDefaultStatus() throws { let data = CLIServerService.errorResponse("bad input") let text = String(data: data, encoding: .utf8)! #expect(text.hasPrefix("HTTP/1.1 400 Bad Request\r\n")) let bodyStart = text.range(of: "\r\n\r\n")!.upperBound let bodyJSON = Data(text[bodyStart...].utf8) let decoded = try JSONDecoder().decode(CLIServerService.AskResponseBody.self, from: bodyJSON) #expect(decoded.error == "bad input") #expect(decoded.response == nil) } @Test("errorResponse supports a custom status code") func errorResponseCustomStatus() { let data = CLIServerService.errorResponse("provider failed", statusCode: 500) let text = String(data: data, encoding: .utf8)! #expect(text.hasPrefix("HTTP/1.1 500 Internal Server Error\r\n")) } @Test("Content-Length in the response header matches the actual JSON body byte count") func responseContentLengthMatchesBody() { let data = CLIServerService.successResponse("hello") let text = String(data: data, encoding: .utf8)! let headerEnd = text.range(of: "\r\n\r\n")!.upperBound let bodyByteCount = Data(text[headerEnd...].utf8).count let lengthLine = text.split(separator: "\r\n").first { $0.hasPrefix("Content-Length:") }! let declaredLength = Int(lengthLine.split(separator: " ")[1])! #expect(declaredLength == bodyByteCount) } }