// // MaterialManagementAPITests.swift // suixinkanTests // import XCTest @testable import suixinkan /// 素材管理 API 路径、参数与请求体测试。 @MainActor final class MaterialManagementAPITests: XCTestCase { func testListBuildsAndroidQueryWithMaterialType() async throws { let session = MockURLSession(responses: [envelopeJSON(#"{"total":0,"list":[],"order":null}"#)]) let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session)) _ = try await api.list(status: 4, keyword: "雪山", page: 2, pageSize: 10) let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url?.path, "/api/app/media-album/list") let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems XCTAssertEqual(query?.first { $0.name == "status" }?.value, "4") XCTAssertEqual(query?.first { $0.name == "keyword" }?.value, "雪山") XCTAssertEqual(query?.first { $0.name == "type" }?.value, "1") XCTAssertEqual(query?.first { $0.name == "page" }?.value, "2") XCTAssertEqual(query?.first { $0.name == "page_size" }?.value, "10") } func testListDecodesNullableOrderAmounts() async throws { let session = MockURLSession(responses: [envelopeJSON(#"{"total":0,"list":[],"order":{"avg_change":-1025,"avg_order_amount":null,"refund_total":null,"total_num":0}}"#)]) let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session)) let response = try await api.list(status: 4, keyword: nil, page: 1, pageSize: 10) XCTAssertEqual(response.order?.avgOrderAmount, "0") XCTAssertEqual(response.order?.refundTotal, "0") XCTAssertEqual(response.order?.avgChange, -1025) XCTAssertEqual(response.order?.totalNum, 0) } func testDetailOperationDeleteAndTagUseAndroidPaths() async throws { let detail = envelopeJSON(#"{"id":8,"status":1,"listing_status":1}"#) let empty = envelopeJSON("{}") let tag = envelopeJSON("23") let session = MockURLSession(responses: [detail, empty, empty, tag]) let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session)) _ = try await api.detail(id: 8) try await api.setListingStatus(id: 8, listingStatus: 0) try await api.delete(id: 8) let tagId = try await api.addTag(name: "snow-tag") XCTAssertEqual(tagId, 23) XCTAssertEqual(session.requests[0].url?.path, "/api/app/media-album/detail") XCTAssertEqual(session.requests[1].url?.path, "/api/app/media-album/operation") XCTAssertEqual(session.requests[2].url?.path, "/api/app/media-album/delete") XCTAssertEqual(session.requests[3].url?.path, "/api/app/media-album/add-tag") XCTAssertTrue(session.requests.prefix(3).allSatisfy { request in URLComponents(url: request.url!, resolvingAgainstBaseURL: false)? .queryItems? .first { $0.name == "type" }? .value == "1" }) } func testUploadAndEditEncodeAndroidBodyKeys() async throws { let session = MockURLSession(responses: [envelopeJSON("{}"), envelopeJSON("{}")]) let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session)) let request = MaterialUploadRequest( name: "素材A", mediaType: .image, mediaList: [ MaterialUploadMediaItem( originalName: "a.jpg", ossUrl: "https://cdn/a.jpg", size: 1024, fileWidthSize: MaterialUploadFileSize(width: 1920, height: 1080) ), ], coverUrl: "https://cdn/c.jpg", coverSize: "512", scenicSpotId: 9, materialTag: "1,2" ) try await api.upload(request) try await api.edit(MaterialUploadRequest( id: 7, name: "素材B", mediaType: .video, mediaList: request.mediaList, coverUrl: request.coverUrl, coverSize: request.coverSize, scenicSpotId: request.scenicSpotId, materialTag: "3" )) let uploadBody = try bodyJSON(session.requests[0]) XCTAssertEqual(session.requests[0].url?.path, "/api/app/media-album/upload") XCTAssertEqual(uploadBody["name"] as? String, "素材A") XCTAssertEqual(uploadBody["type"] as? String, "1") XCTAssertEqual(uploadBody["media_type"] as? String, "1") XCTAssertNil(uploadBody["project_id"]) XCTAssertEqual(uploadBody["material_tag"] as? String, "1,2") let media = (uploadBody["media_list"] as? [[String: Any]])?.first XCTAssertEqual(media?["original_name"] as? String, "a.jpg") XCTAssertEqual(media?["oss_url"] as? String, "https://cdn/a.jpg") let editBody = try bodyJSON(session.requests[1]) XCTAssertEqual(session.requests[1].url?.path, "/api/app/media-album/edit") XCTAssertEqual(editBody["id"] as? Int, 7) XCTAssertEqual(editBody["type"] as? String, "1") XCTAssertEqual(editBody["media_type"] as? String, "2") XCTAssertNil(editBody["project_id"]) } func testMaterialOSSObjectKeyMatchesAndroidFolderLayout() { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(secondsFromGMT: 0)! let date = calendar.date(from: DateComponents(year: 2026, month: 7, day: 13))! let uuid = UUID(uuidString: "11111111-2222-3333-4444-555555555555")! let key = OSSUploadPolicy.objectKey( fileName: "video/demo.mp4", scenicId: 9, moduleType: "material", date: date, uuid: uuid, timeZone: TimeZone(secondsFromGMT: 0)! ) XCTAssertEqual(key, "material/9/0/11111111222233334444555555555555_video_demo.mp4") } private func envelopeJSON(_ dataJSON: String) -> Data { """ {"code":100000,"msg":"success","data":\(dataJSON)} """.data(using: .utf8)! } private func bodyJSON(_ request: URLRequest) throws -> [String: Any] { try XCTUnwrap(JSONSerialization.jsonObject(with: try XCTUnwrap(request.httpBody)) as? [String: Any]) } }