743 lines
31 KiB
Objective-C
743 lines
31 KiB
Objective-C
//
|
||
// HttpdnsNWHTTPClientTests.m
|
||
// TrustHttpDNSTests
|
||
//
|
||
// @author Created by Claude Code on 2025-11-01
|
||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||
//
|
||
|
||
#import <XCTest/XCTest.h>
|
||
#import <OCMock/OCMock.h>
|
||
#import "HttpdnsNWHTTPClient.h"
|
||
#import "HttpdnsNWHTTPClient_Internal.h"
|
||
#import "HttpdnsNWReusableConnection.h"
|
||
#import "HttpdnsNWHTTPClientTestHelper.h"
|
||
|
||
@interface HttpdnsNWHTTPClientTests : XCTestCase
|
||
|
||
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
|
||
|
||
@end
|
||
|
||
@implementation HttpdnsNWHTTPClientTests
|
||
|
||
- (void)setUp {
|
||
[super setUp];
|
||
self.client = [[HttpdnsNWHTTPClient alloc] init];
|
||
}
|
||
|
||
- (void)tearDown {
|
||
self.client = nil;
|
||
[super tearDown];
|
||
}
|
||
|
||
#pragma mark - A. HTTP è§£æž<C3A6>逻辑测试
|
||
|
||
#pragma mark - A1. Header è§£æž<C3A6> (9ä¸?
|
||
|
||
// A1.1 æ£å¸¸å“<C3A5>应
|
||
- (void)testParseHTTPHeaders_ValidResponse_Success {
|
||
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||
statusText:@"OK"
|
||
headers:@{@"Content-Type": @"application/json"}
|
||
body:nil];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||
XCTAssertEqual(statusCode, 200);
|
||
XCTAssertNotNil(headers);
|
||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // key 应该转为å°<C3A5>写
|
||
XCTAssertNil(error);
|
||
}
|
||
|
||
// A1.2 多个头部
|
||
- (void)testParseHTTPHeaders_MultipleHeaders_AllParsed {
|
||
NSDictionary *testHeaders = @{
|
||
@"Content-Type": @"application/json",
|
||
@"Content-Length": @"123",
|
||
@"Connection": @"keep-alive",
|
||
@"X-Custom-Header": @"custom-value",
|
||
@"Cache-Control": @"no-cache"
|
||
};
|
||
|
||
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||
statusText:@"OK"
|
||
headers:testHeaders
|
||
body:nil];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||
XCTAssertEqual(headers.count, testHeaders.count);
|
||
// 验è¯<C3A8>所有头部都被解æž<C3A6>,ä¸?key 转为å°<C3A5>写
|
||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
|
||
XCTAssertEqualObjects(headers[@"content-length"], @"123");
|
||
XCTAssertEqualObjects(headers[@"connection"], @"keep-alive");
|
||
XCTAssertEqualObjects(headers[@"x-custom-header"], @"custom-value");
|
||
XCTAssertEqualObjects(headers[@"cache-control"], @"no-cache");
|
||
}
|
||
|
||
// A1.3 ä¸<C3A4>完整å“<C3A5>åº?
|
||
- (void)testParseHTTPHeaders_IncompleteData_ReturnsIncomplete {
|
||
NSString *incompleteResponse = @"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n";
|
||
NSData *data = [incompleteResponse dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultIncomplete);
|
||
}
|
||
|
||
// A1.4 æ— æ•ˆçŠ¶æ€<C3A6>行
|
||
- (void)testParseHTTPHeaders_InvalidStatusLine_ReturnsError {
|
||
NSString *invalidResponse = @"INVALID\r\n\r\n";
|
||
NSData *data = [invalidResponse dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
|
||
XCTAssertNotNil(error);
|
||
}
|
||
|
||
// A1.5 头部包å<E280A6>«ç©ºæ ¼
|
||
- (void)testParseHTTPHeaders_HeadersWithWhitespace_Trimmed {
|
||
NSString *responseWithSpaces = @"HTTP/1.1 200 OK\r\nContent-Type: application/json \r\n\r\n";
|
||
NSData *data = [responseWithSpaces dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // 应该�trim
|
||
}
|
||
|
||
// A1.6 头部没有�
|
||
- (void)testParseHTTPHeaders_EmptyHeaderValue_HandledGracefully {
|
||
NSString *responseWithEmptyValue = @"HTTP/1.1 200 OK\r\nX-Empty-Header:\r\n\r\n";
|
||
NSData *data = [responseWithEmptyValue dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||
XCTAssertEqualObjects(headers[@"x-empty-header"], @"");
|
||
}
|
||
|
||
// A1.7 状æ€<C3A6>ç <C3A7>é<EFBFBD>žæ•°å?
|
||
- (void)testParseHTTPHeaders_NonNumericStatusCode_ReturnsError {
|
||
NSString *invalidStatusCode = @"HTTP/1.1 ABC OK\r\n\r\n";
|
||
NSData *data = [invalidStatusCode dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
|
||
}
|
||
|
||
// A1.8 状æ€<C3A6>ç <C3A7>为零
|
||
- (void)testParseHTTPHeaders_StatusCodeZero_ReturnsError {
|
||
NSString *zeroStatusCode = @"HTTP/1.1 0 OK\r\n\r\n";
|
||
NSData *data = [zeroStatusCode dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
|
||
}
|
||
|
||
// A1.9 头部没有冒å<E28099>·è¢«è·³è¿?
|
||
- (void)testParseHTTPHeaders_HeaderWithoutColon_Skipped {
|
||
NSString *responseWithInvalidHeader = @"HTTP/1.1 200 OK\r\nInvalidHeader\r\nContent-Type: application/json\r\n\r\n";
|
||
NSData *data = [responseWithInvalidHeader dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex;
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEndIndex
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // 有效头部æ£å¸¸è§£æž<C3A6>
|
||
}
|
||
|
||
#pragma mark - A2. Chunked ç¼–ç <C3A7>检æŸ?(8ä¸?
|
||
|
||
// A2.1 å<>•个 chunk
|
||
- (void)testCheckChunkedBody_SingleChunk_DetectsComplete {
|
||
NSString *singleChunkBody = @"5\r\nhello\r\n0\r\n\r\n";
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", singleChunkBody];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||
XCTAssertNil(error);
|
||
}
|
||
|
||
// A2.2 多个 chunks
|
||
- (void)testCheckChunkedBody_MultipleChunks_DetectsComplete {
|
||
NSString *multiChunkBody = @"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", multiChunkBody];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||
XCTAssertNil(error);
|
||
}
|
||
|
||
// A2.3 ä¸<C3A4>完æ•?chunk
|
||
- (void)testCheckChunkedBody_IncompleteChunk_ReturnsIncomplete {
|
||
NSString *incompleteChunkBody = @"5\r\nhel"; // æ•°æ<C2B0>®ä¸<C3A4>完æ•?
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", incompleteChunkBody];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultIncomplete);
|
||
}
|
||
|
||
// A2.4 �chunk extension
|
||
- (void)testCheckChunkedBody_WithChunkExtension_Ignored {
|
||
NSString *chunkWithExtension = @"5;name=value\r\nhello\r\n0\r\n\r\n";
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithExtension];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
// headerEndIndex 指å<E280A1>‘第一ä¸?\r\n\r\n ä¸çš„第一ä¸?\r
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||
XCTAssertNil(error);
|
||
}
|
||
|
||
// A2.5 æ— æ•ˆå<CB86><C3A5>å…进制 size
|
||
- (void)testCheckChunkedBody_InvalidHexSize_ReturnsError {
|
||
NSString *invalidChunkSize = @"ZZZ\r\nhello\r\n0\r\n\r\n";
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", invalidChunkSize];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
|
||
XCTAssertNotNil(error);
|
||
}
|
||
|
||
// A2.6 Chunk size 溢出
|
||
- (void)testCheckChunkedBody_ChunkSizeOverflow_ReturnsError {
|
||
NSString *overflowChunkSize = @"FFFFFFFFFFFFFFFF\r\n";
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", overflowChunkSize];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
|
||
XCTAssertNotNil(error);
|
||
}
|
||
|
||
// A2.7 缺少 CRLF 终æ¢ç¬?
|
||
- (void)testCheckChunkedBody_MissingCRLFTerminator_ReturnsError {
|
||
NSString *missingTerminator = @"5\r\nhelloXX0\r\n\r\n"; // 应该�hello\r\n
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", missingTerminator];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
|
||
XCTAssertNotNil(error);
|
||
}
|
||
|
||
// A2.8 �trailers
|
||
- (void)testCheckChunkedBody_WithTrailers_DetectsComplete {
|
||
NSString *chunkWithTrailers = @"5\r\nhello\r\n0\r\nX-Trailer: value\r\nX-Custom: test\r\n\r\n";
|
||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithTrailers];
|
||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||
NSError *error;
|
||
|
||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||
headerEndIndex:headerEndIndex
|
||
error:&error];
|
||
|
||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||
XCTAssertNil(error);
|
||
}
|
||
|
||
#pragma mark - A3. Chunked è§£ç <C3A7> (2ä¸?
|
||
|
||
// A3.1 多个 chunks è§£ç <C3A7>
|
||
- (void)testDecodeChunkedBody_MultipleChunks_DecodesCorrectly {
|
||
NSArray *chunks = @[
|
||
[@"hello" dataUsingEncoding:NSUTF8StringEncoding],
|
||
[@" world" dataUsingEncoding:NSUTF8StringEncoding]
|
||
];
|
||
|
||
NSData *chunkedData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
|
||
headers:nil
|
||
chunks:chunks];
|
||
|
||
// æ<><C3A6>å<EFBFBD>– chunked body 部分(跳è¿?headersï¼?
|
||
NSData *headerData = [@"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
|
||
NSData *bodyData = [chunkedData subdataWithRange:NSMakeRange(headerData.length, chunkedData.length - headerData.length)];
|
||
|
||
NSError *error;
|
||
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
|
||
|
||
XCTAssertNotNil(decoded);
|
||
XCTAssertNil(error);
|
||
NSString *decodedString = [[NSString alloc] initWithData:decoded encoding:NSUTF8StringEncoding];
|
||
XCTAssertEqualObjects(decodedString, @"hello world");
|
||
}
|
||
|
||
// A3.2 æ— æ•ˆæ ¼å¼<C3A5>返回 nil
|
||
- (void)testDecodeChunkedBody_InvalidFormat_ReturnsNil {
|
||
NSString *invalidChunked = @"ZZZ\r\nbad data\r\n";
|
||
NSData *bodyData = [invalidChunked dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSError *error;
|
||
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
|
||
|
||
XCTAssertNil(decoded);
|
||
XCTAssertNotNil(error);
|
||
}
|
||
|
||
#pragma mark - A4. 完整å“<C3A5>应解æž<C3A6> (6ä¸?
|
||
|
||
// A4.1 Content-Length å“<C3A5>应
|
||
- (void)testParseResponse_WithContentLength_ParsesCorrectly {
|
||
NSString *bodyString = @"{\"ips\":[]}";
|
||
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
|
||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||
statusText:@"OK"
|
||
headers:@{@"Content-Type": @"application/json"}
|
||
body:bodyData];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success);
|
||
XCTAssertEqual(statusCode, 200);
|
||
XCTAssertNotNil(headers);
|
||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
|
||
XCTAssertEqualObjects(body, bodyData);
|
||
XCTAssertNil(error);
|
||
}
|
||
|
||
// A4.2 Chunked å“<C3A5>应
|
||
- (void)testParseResponse_WithChunkedEncoding_DecodesBody {
|
||
NSArray *chunks = @[
|
||
[@"{\"ips\"" dataUsingEncoding:NSUTF8StringEncoding],
|
||
[@":[]}" dataUsingEncoding:NSUTF8StringEncoding]
|
||
];
|
||
|
||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
|
||
headers:@{@"Content-Type": @"application/json"}
|
||
chunks:chunks];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success);
|
||
XCTAssertEqual(statusCode, 200);
|
||
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
|
||
XCTAssertEqualObjects(bodyString, @"{\"ips\":[]}");
|
||
}
|
||
|
||
// A4.3 ç©?body
|
||
- (void)testParseResponse_EmptyBody_Success {
|
||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:204
|
||
statusText:@"No Content"
|
||
headers:nil
|
||
body:nil];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success);
|
||
XCTAssertEqual(statusCode, 204);
|
||
XCTAssertEqual(body.length, 0);
|
||
}
|
||
|
||
// A4.4 Content-Length ä¸<C3A4>匹é…<C3A9>ä»<C3A4>ç„¶æˆ<C3A6>åŠ?
|
||
- (void)testParseResponse_ContentLengthMismatch_LogsButSucceeds {
|
||
NSData *bodyData = [@"short" dataUsingEncoding:NSUTF8StringEncoding];
|
||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||
statusText:@"OK"
|
||
headers:@{@"Content-Length": @"100"} // ä¸<C3A4>匹é…?
|
||
body:bodyData];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success); // ä»<C3A4>ç„¶æˆ<C3A6>功,å<C592>ªæ˜¯æ—¥å¿—è¦å‘?
|
||
XCTAssertEqualObjects(body, bodyData);
|
||
}
|
||
|
||
// A4.5 空数æ<C2B0>®è¿”回错è¯?
|
||
- (void)testParseResponse_EmptyData_ReturnsError {
|
||
NSData *emptyData = [NSData data];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:emptyData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertFalse(success);
|
||
XCTAssertNotNil(error);
|
||
}
|
||
|
||
// A4.6 å<>ªæœ‰ headers æ—?body
|
||
- (void)testParseResponse_OnlyHeaders_EmptyBody {
|
||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||
statusText:@"OK"
|
||
headers:@{@"Content-Type": @"text/plain"}
|
||
body:nil];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success);
|
||
XCTAssertEqual(statusCode, 200);
|
||
XCTAssertNotNil(headers);
|
||
XCTAssertEqual(body.length, 0);
|
||
}
|
||
|
||
#pragma mark - C. 请求构建测试 (7�
|
||
|
||
// C.1 基本 GET 请求
|
||
- (void)testBuildHTTPRequest_BasicGET_CorrectFormat {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"TestAgent"];
|
||
|
||
XCTAssertTrue([request containsString:@"GET / HTTP/1.1\r\n"]);
|
||
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
|
||
XCTAssertTrue([request containsString:@"User-Agent: TestAgent\r\n"]);
|
||
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
|
||
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
|
||
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
|
||
XCTAssertTrue([request hasSuffix:@"\r\n\r\n"]);
|
||
}
|
||
|
||
// C.2 带查询å<C2A2>‚æ•?
|
||
- (void)testBuildHTTPRequest_WithQueryString_Included {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com/path?foo=bar&baz=qux"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
|
||
XCTAssertTrue([request containsString:@"GET /path?foo=bar&baz=qux HTTP/1.1\r\n"]);
|
||
}
|
||
|
||
// C.3 包å<E280A6>« User-Agent
|
||
- (void)testBuildHTTPRequest_WithUserAgent_Included {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"CustomAgent/1.0"];
|
||
|
||
XCTAssertTrue([request containsString:@"User-Agent: CustomAgent/1.0\r\n"]);
|
||
}
|
||
|
||
// C.4 HTTP 默认端å<C2AF>£ä¸<C3A4>显ç¤?
|
||
- (void)testBuildHTTPRequest_HTTPDefaultPort_NotInHost {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com:80/"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
|
||
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
|
||
XCTAssertFalse([request containsString:@"Host: example.com:80\r\n"]);
|
||
}
|
||
|
||
// C.5 HTTPS 默认端å<C2AF>£ä¸<C3A4>显ç¤?
|
||
- (void)testBuildHTTPRequest_HTTPSDefaultPort_NotInHost {
|
||
NSURL *url = [NSURL URLWithString:@"https://example.com:443/"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
|
||
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
|
||
XCTAssertFalse([request containsString:@"Host: example.com:443\r\n"]);
|
||
}
|
||
|
||
// C.6 é<>žé»˜è®¤ç«¯å<C2AF>£æ˜¾ç¤?
|
||
- (void)testBuildHTTPRequest_NonDefaultPort_InHost {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com:8080/"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
|
||
XCTAssertTrue([request containsString:@"Host: example.com:8080\r\n"]);
|
||
}
|
||
|
||
// C.7 固定头部å˜åœ¨
|
||
- (void)testBuildHTTPRequest_FixedHeaders_Present {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
|
||
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
|
||
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
|
||
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
|
||
}
|
||
|
||
#pragma mark - E. TLS 验è¯<C3A8>测试 (4个å<C2AA> ä½<C3A4>符)
|
||
|
||
// 注æ„<C3A6>:TLS 验è¯<C3A8>测试需è¦<C3A8>真实的 SecTrustRef 或å¤<C3A5>æ<EFBFBD>‚çš„ mock
|
||
// 这些测试在实际环境ä¸éœ€è¦<C3A8>æ ¹æ<C2B9>®æµ‹è¯•框架调æ•?
|
||
|
||
// E.1 有效è¯<C3A8>书返回 YES
|
||
- (void)testEvaluateServerTrust_ValidCertificate_ReturnsYES {
|
||
// 需è¦<C3A8>创建有效的 SecTrustRef 进行测试
|
||
// è·³è¿‡æˆ–æ ‡è®°ä¸ºæ‰‹åŠ¨æµ‹è¯•
|
||
}
|
||
|
||
// E.2 Proceed 结果返回 YES
|
||
- (void)testEvaluateServerTrust_ProceedResult_ReturnsYES {
|
||
// Mock SecTrustEvaluate 返回 kSecTrustResultProceed
|
||
}
|
||
|
||
// E.3 æ— æ•ˆè¯<C3A8>书返回 NO
|
||
- (void)testEvaluateServerTrust_InvalidCertificate_ReturnsNO {
|
||
// Mock SecTrustEvaluate 返回 kSecTrustResultDeny
|
||
}
|
||
|
||
// E.4 指定域å<C5B8><C3A5>使用 SSL Policy
|
||
- (void)testEvaluateServerTrust_WithDomain_UsesSSLPolicy {
|
||
// 验è¯<C3A8>使用äº?SecPolicyCreateSSL(true, domain)
|
||
}
|
||
|
||
#pragma mark - F. 边缘情况测试 (5�
|
||
|
||
// F.1 è¶…é•¿ URL
|
||
- (void)testPerformRequest_VeryLongURL_HandlesCorrectly {
|
||
NSMutableString *longPath = [NSMutableString stringWithString:@"http://example.com/"];
|
||
for (int i = 0; i < 1000; i++) {
|
||
[longPath appendString:@"long/"];
|
||
}
|
||
|
||
NSURL *url = [NSURL URLWithString:longPath];
|
||
XCTAssertNotNil(url);
|
||
|
||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
XCTAssertTrue(request.length > 5000);
|
||
}
|
||
|
||
// F.2 ç©?User-Agent
|
||
- (void)testBuildRequest_EmptyUserAgent_NoUserAgentHeader {
|
||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||
NSString *requestWithNil = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||
|
||
XCTAssertFalse([requestWithNil containsString:@"User-Agent:"]);
|
||
}
|
||
|
||
// F.3 超大å“<C3A5>应ä½?
|
||
- (void)testParseResponse_VeryLargeBody_HandlesCorrectly {
|
||
NSData *largeBody = [HttpdnsNWHTTPClientTestHelper randomDataWithSize:5 * 1024 * 1024];
|
||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||
statusText:@"OK"
|
||
headers:nil
|
||
body:largeBody];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success);
|
||
XCTAssertEqual(body.length, largeBody.length);
|
||
}
|
||
|
||
// F.4 Chunked è§£ç <C3A7>失败回退到原始数æ<C2B0>?
|
||
- (void)testParseResponse_ChunkedDecodeFails_FallsBackToRaw {
|
||
NSString *badChunked = @"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nBAD_CHUNK_DATA";
|
||
NSData *responseData = [badChunked dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
||
NSInteger statusCode;
|
||
NSDictionary *headers;
|
||
NSData *body;
|
||
NSError *error;
|
||
|
||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||
statusCode:&statusCode
|
||
headers:&headers
|
||
body:&body
|
||
error:&error];
|
||
|
||
XCTAssertTrue(success);
|
||
XCTAssertNotNil(body);
|
||
}
|
||
|
||
// F.5 连接æ±?key 生æˆ<C3A6>测试
|
||
- (void)testConnectionPoolKey_DifferentHosts_SeparateKeys {
|
||
NSString *key1 = [self.client connectionPoolKeyForHost:@"host1.com" port:@"80" useTLS:NO];
|
||
NSString *key2 = [self.client connectionPoolKeyForHost:@"host2.com" port:@"80" useTLS:NO];
|
||
|
||
XCTAssertNotEqualObjects(key1, key2);
|
||
}
|
||
|
||
- (void)testConnectionPoolKey_DifferentPorts_SeparateKeys {
|
||
NSString *key1 = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
|
||
NSString *key2 = [self.client connectionPoolKeyForHost:@"example.com" port:@"8080" useTLS:NO];
|
||
|
||
XCTAssertNotEqualObjects(key1, key2);
|
||
}
|
||
|
||
- (void)testConnectionPoolKey_HTTPvsHTTPS_SeparateKeys {
|
||
NSString *keyHTTP = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
|
||
NSString *keyHTTPS = [self.client connectionPoolKeyForHost:@"example.com" port:@"443" useTLS:YES];
|
||
|
||
XCTAssertNotEqualObjects(keyHTTP, keyHTTPS);
|
||
}
|
||
|
||
@end
|