// // UploadTests.swift // suixinkanTests // // Created by Codex on 2026/6/22. // import XCTest @testable import suixinkan_ios @MainActor /// 上传模块测试,覆盖 OSS 上传策略和 STS 配置接口。 final class UploadTests: XCTestCase { /// 测试文件校验能识别空文件、超大文件和不支持扩展名。 func testOSSUploadPolicyValidatesFileConstraints() { XCTAssertThrowsError(try OSSUploadPolicy.validate(dataSize: 0, fileName: "avatar.jpg")) { error in XCTAssertEqual(error as? OSSUploadError, .emptyFile) } XCTAssertThrowsError(try OSSUploadPolicy.validate(dataSize: OSSUploadPolicy.maxFileSize + 1, fileName: "avatar.jpg")) { error in XCTAssertEqual(error as? OSSUploadError, .fileTooLarge) } XCTAssertThrowsError(try OSSUploadPolicy.validate(dataSize: 12, fileName: "avatar.txt")) { error in XCTAssertEqual(error as? OSSUploadError, .unsupportedFileType) } XCTAssertNoThrow(try OSSUploadPolicy.validate(dataSize: 12, fileName: "avatar.heic")) } /// 测试 objectKey 按业务模块生成稳定路径并清洗文件名。 func testOSSUploadPolicyBuildsObjectKeys() { let date = Date(timeIntervalSince1970: 1_767_225_600) let uuid = UUID(uuidString: "12345678-1234-1234-1234-1234567890ab")! let timeZone = TimeZone(secondsFromGMT: 0)! XCTAssertEqual( OSSUploadPolicy.objectKey(fileName: "a/b.jpg", scenicId: 9, moduleType: "user_avatar", date: date, uuid: uuid, timeZone: timeZone), "avatar/20260101/9/123456781234123412341234567890AB_a_b.jpg" ) XCTAssertEqual( OSSUploadPolicy.objectKey(fileName: "id.jpg", scenicId: 9, moduleType: "real_name", date: date, uuid: uuid, timeZone: timeZone), "real_name/20260101/9/123456781234123412341234567890AB_id.jpg" ) XCTAssertEqual( OSSUploadPolicy.objectKey(fileName: "video.mp4", scenicId: 9, moduleType: "task_upload", date: date, uuid: uuid, timeZone: timeZone), "task_upload/20260101/9/123456781234123412341234567890AB_video.mp4" ) XCTAssertEqual( OSSUploadPolicy.objectKey(fileName: "bank.jpg", scenicId: 9, moduleType: "bank_card", date: date, uuid: uuid, timeZone: timeZone), "bank_card/20260101/9/123456781234123412341234567890AB_bank.jpg" ) XCTAssertEqual( OSSUploadPolicy.objectKey(fileName: "cover.jpg", scenicId: 9, moduleType: "project", date: date, uuid: uuid, timeZone: timeZone), "project/20260101/9/123456781234123412341234567890AB_cover.jpg" ) XCTAssertEqual( OSSUploadPolicy.objectKey(fileName: "live.mp4", scenicId: 9, moduleType: "alive_album", date: date, uuid: uuid, timeZone: timeZone), "live_albums/20260101/9/123456781234123412341234567890AB_live.mp4" ) } /// 测试 MIME 类型推断和 URL 拼接规则。 func testOSSUploadPolicyContentTypeAndURLJoin() { XCTAssertEqual(OSSUploadPolicy.contentType(for: "a.jpg", fileType: 2), "image/jpeg") XCTAssertEqual(OSSUploadPolicy.contentType(for: "a.png", fileType: 2), "image/png") XCTAssertEqual(OSSUploadPolicy.contentType(for: "a.mp4", fileType: 1), "video/mp4") XCTAssertEqual(OSSUploadPolicy.contentType(for: "a.unknown", fileType: 1), "video/mp4") XCTAssertEqual(OSSUploadPolicy.contentType(for: "a.unknown", fileType: 2), "image/jpeg") XCTAssertEqual(OSSUploadPolicy.joinURL(baseURL: "https://cdn.example.com", objectKey: "a/b.jpg"), "https://cdn.example.com/a/b.jpg") XCTAssertEqual(OSSUploadPolicy.joinURL(baseURL: "https://cdn.example.com/", objectKey: "a/b.jpg"), "https://cdn.example.com/a/b.jpg") } /// 测试 STS token 接口使用正确路径、bucket 参数并兼容宽松字段类型。 func testUploadAPIRequestsSTSConfig() async throws { let response = """ { "code": 100000, "msg": "success", "data": { "base_url": 123, "endpoint": "oss-cn-hangzhou.aliyuncs.com", "region": "cn-hangzhou", "bucket": "vipsky", "expire_seconds": "900", "credentials": { "access_key_id": 456, "access_key_secret": "secret", "security_token": "token" } } } """.data(using: .utf8)! let session = UploadURLSession(responses: [response]) let api = UploadAPI(client: APIClient(session: session)) let config = try await api.aliyunOSSBucket() XCTAssertEqual(config.baseUrl, "123") XCTAssertEqual(config.expireSeconds, 900) XCTAssertEqual(config.credentials.accessKeyId, "456") let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url?.path, "/api/app/config/get-sts-token") XCTAssertEqual(URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?.queryItems?.first?.value, "vipsky") } } /// 上传测试用 URLSession 替身,按顺序返回预设响应。 private final class UploadURLSession: URLSessionProtocol { private var responses: [Data] private(set) var requests: [URLRequest] = [] /// 初始化顺序响应。 init(responses: [Data]) { self.responses = responses } /// 记录请求并返回下一份响应。 func data(for request: URLRequest) async throws -> (Data, URLResponse) { requests.append(request) let data = responses.isEmpty ? Data() : responses.removeFirst() return ( data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! ) } }