新增 OSS 上传与 Kingfisher 图片加载支持

This commit is contained in:
2026-06-22 16:30:05 +08:00
parent 08465fffde
commit 726f19c124
20 changed files with 1399 additions and 134 deletions

View File

@ -0,0 +1,113 @@
//
// UploadTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@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"
)
}
/// 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)!
)
}
}