EMASCurlTests/EMASCurlUploadTest.m (219 lines of code) (raw):
//
// EMASCurlUploadTest.m
// EMASCurlTests
//
// Created by xuyecan on 2024/12/16.
//
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import <EMASCurl/EMASCurl.h>
#import "EMASCurlTestConstants.h"
@interface EMASCurlUploadTestBase : XCTestCase
@property (nonatomic, strong) NSMutableArray<NSNumber *> *progressValues;
@property (nonatomic, copy) void (^completionBlock)(NSData *data, NSURLResponse *response, NSError *error);
@end
static NSURLSession *session;
@implementation EMASCurlUploadTestBase
- (void)setUp {
[super setUp];
self.progressValues = [NSMutableArray array];
}
- (NSData *)generateTestData:(NSUInteger)size {
NSMutableData *data = [NSMutableData dataWithCapacity:size];
for (NSUInteger i = 0; i < size; i++) {
uint8_t byte = (uint8_t)(i % 256);
[data appendBytes:&byte length:1];
}
return data;
}
- (NSString *)createTemporaryFileWithData:(NSData *)data {
NSString *tempDir = NSTemporaryDirectory();
NSString *fileName = [NSString stringWithFormat:@"upload_test_%@.bin", [[NSUUID UUID] UUIDString]];
NSString *filePath = [tempDir stringByAppendingPathComponent:fileName];
[data writeToFile:filePath atomically:YES];
return filePath;
}
- (NSString *)createMultipartFormDataFileWithData:(NSData *)data filename:(NSString *)filename {
NSString *boundary = @"Boundary-EMASCurlUploadTest";
NSMutableData *formData = [NSMutableData data];
// Add form field boundary
[formData appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
// Add content disposition header
[formData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"%@\"\r\n", filename] dataUsingEncoding:NSUTF8StringEncoding]];
// Add content type header
[formData appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
// Add file data
[formData appendData:data];
// Add closing boundary
[formData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
// Write to temporary file
NSString *tempDir = NSTemporaryDirectory();
NSString *formDataPath = [tempDir stringByAppendingPathComponent:[NSString stringWithFormat:@"form_data_%@", [[NSUUID UUID] UUIDString]]];
[formData writeToFile:formDataPath atomically:YES];
return formDataPath;
}
- (void)uploadData:(NSString *)endpoint {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", endpoint, PATH_UPLOAD_POST_SLOW]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
NSData *testData = [self generateTestData:1024 * 1024];
NSString *formDataPath = [self createMultipartFormDataFileWithData:testData filename:@"test.bin"];
NSURL *fileURL = [NSURL fileURLWithPath:formDataPath];
NSString *boundary = @"Boundary-EMASCurlUploadTest";
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromFile:fileURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[[NSFileManager defaultManager] removeItemAtPath:formDataPath error:nil];
XCTAssertNil(error, @"Upload failed with error: %@", error);
XCTAssertNotNil(response, @"No response received");
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
XCTAssertEqual(httpResponse.statusCode, 200, @"Expected 200 status code");
NSError *jsonError;
NSDictionary *responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
XCTAssertNil(jsonError, @"Failed to parse response JSON");
XCTAssertNotNil(responseData[@"size"], @"Response should contain file size");
XCTAssertEqual([responseData[@"size"] integerValue], 1024 * 1024, @"File size should be exactly 1MB");
dispatch_semaphore_signal(semaphore);
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)uploadDataWithProgress:(NSString *)endpoint {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", endpoint, PATH_UPLOAD_POST_SLOW]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
NSData *testData = [self generateTestData:1024 * 1024];
NSString *formDataPath = [self createMultipartFormDataFileWithData:testData filename:@"test.bin"];
NSURL *fileURL = [NSURL fileURLWithPath:formDataPath];
NSString *boundary = @"Boundary-EMASCurlUploadTest";
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
self.progressValues = [NSMutableArray array];
__weak typeof(self) weakSelf = self;
[EMASCurlProtocol setUploadProgressUpdateBlockForRequest:request uploadProgressUpdateBlock:^(NSURLRequest * _Nonnull request, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
typeof(self) strongSelf = weakSelf;
double progress = (double)totalBytesSent / totalBytesExpectedToSend;
[strongSelf.progressValues addObject:@(progress)];
}];
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromFile:fileURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
[[NSFileManager defaultManager] removeItemAtPath:formDataPath error:nil];
XCTAssertNil(error, @"Upload failed with error: %@", error);
XCTAssertNotNil(response, @"No response received");
typeof(self) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, @"Self was deallocated");
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
XCTAssertEqual(httpResponse.statusCode, 200, @"Expected 200 status code");
NSError *jsonError;
NSDictionary *responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
XCTAssertNil(jsonError, @"Failed to parse response JSON");
XCTAssertNotNil(responseData[@"size"], @"Response should contain file size");
XCTAssertEqual([responseData[@"size"] integerValue], 1024 * 1024, @"File size should be exactly 1MB");
XCTAssertGreaterThan(strongSelf.progressValues.count, 0, @"Should have received progress updates");
XCTAssertEqualWithAccuracy([[strongSelf.progressValues lastObject] doubleValue], 1.0, 0.01, @"Final progress should be 100%");
double previousProgress = 0;
for (NSNumber *progress in strongSelf.progressValues) {
XCTAssertGreaterThanOrEqual([progress doubleValue], previousProgress, @"Progress should increase monotonically");
previousProgress = [progress doubleValue];
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
XCTAssertEqual(dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), 0, @"Upload request timed out");
}
- (void)uploadDataAndCancel:(NSString *)endpoint {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", endpoint, PATH_UPLOAD_POST_SLOW]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
NSData *testData = [self generateTestData:1024 * 1024];
NSString *formDataPath = [self createMultipartFormDataFileWithData:testData filename:@"test_large.bin"];
NSURL *fileURL = [NSURL fileURLWithPath:formDataPath];
NSString *boundary = @"Boundary-EMASCurlUploadTest";
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
self.progressValues = [NSMutableArray array];
__weak typeof(self) weakSelf = self;
__block NSMutableArray<NSURLSessionUploadTask *> *requestWrapper = [NSMutableArray new];
[EMASCurlProtocol setUploadProgressUpdateBlockForRequest:request uploadProgressUpdateBlock:^(NSURLRequest * _Nonnull request, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
double progress = 0;
if (totalBytesExpectedToSend > 0) {
progress = (double)totalBytesSent / totalBytesExpectedToSend;
}
typeof(self) strongSelf = weakSelf;
[strongSelf.progressValues addObject:@(progress)];
if (progress > 0.3) {
[requestWrapper[0] cancel];
}
}];
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromFile:fileURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
[[NSFileManager defaultManager] removeItemAtPath:formDataPath error:nil];
XCTAssertNotNil(error, @"Expected error due to cancellation");
XCTAssertEqual(error.code, -999, @"Expected cancellation error code");
typeof(self) strongSelf = weakSelf;
XCTAssertLessThan([[strongSelf.progressValues lastObject] doubleValue], 0.5, @"Final progress should be less than 50%");
dispatch_semaphore_signal(semaphore);
}];
[requestWrapper addObject:task];
[task resume];
XCTAssertEqual(dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), 0, @"Upload request timed out");
}
- (void)uploadDataUsingHttpBody:(NSString *)endpoint {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", endpoint, PATH_UPLOAD_PUT_SLOW]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"PUT";
NSData *testData = [self generateTestData:1024 * 1024];
[request setHTTPBody:testData];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
XCTAssertNil(error, @"Upload failed with error: %@", error);
XCTAssertNotNil(response, @"No response received");
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
XCTAssertEqual(httpResponse.statusCode, 200, @"Expected 200 status code");
dispatch_semaphore_signal(semaphore);
}];
[dataTask resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end
@interface EMASCurlUploadTestHttp11 : EMASCurlUploadTestBase
@end
@implementation EMASCurlUploadTestHttp11
+ (void)setUp {
[EMASCurlProtocol setDebugLogEnabled:YES];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
[EMASCurlProtocol installIntoSessionConfiguration:config];
session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
}
- (void)testUploadData {
[self uploadData:HTTP11_ENDPOINT];
}
- (void)testUploadDataWithProgress {
[self uploadDataWithProgress:HTTP11_ENDPOINT];
}
- (void)testUploadDataAndCancel {
[self uploadDataAndCancel:HTTP11_ENDPOINT];
}
- (void)testCancelUploadAndUploadAgain {
[self uploadDataAndCancel:HTTP11_ENDPOINT];
[self uploadData:HTTP11_ENDPOINT];
}
- (void)testUploadDataUsingHttpBody {
[self uploadDataUsingHttpBody:HTTP11_ENDPOINT];
}
@end
@interface EMASCurlUploadTestHttp2 : EMASCurlUploadTestBase
@end
@implementation EMASCurlUploadTestHttp2
+ (void)setUp {
[EMASCurlProtocol setDebugLogEnabled:YES];
[EMASCurlProtocol setHTTPVersion:HTTP2];
NSBundle *testBundle = [NSBundle bundleForClass:[self class]];
NSString *certPath = [testBundle pathForResource:@"ca" ofType:@"crt"];
XCTAssertNotNil(certPath, @"Certificate file not found in test bundle.");
[EMASCurlProtocol setSelfSignedCAFilePath:certPath];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
[EMASCurlProtocol installIntoSessionConfiguration:config];
session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
}
- (void)testUploadData {
[self uploadData:HTTP2_ENDPOINT];
}
- (void)testUploadDataWithProgress {
[self uploadDataWithProgress:HTTP2_ENDPOINT];
}
- (void)testUploadDataAndCancel {
[self uploadDataAndCancel:HTTP2_ENDPOINT];
}
- (void)testCancelUploadAndUploadAgain {
[self uploadDataAndCancel:HTTP2_ENDPOINT];
[self uploadData:HTTP2_ENDPOINT];
}
@end