Add TravelAlbum, ProfileSpace, and wired camera transfer modules.
Introduce travel album entry with Sony PTP tethering pipeline, profile space settings page, home routing updates, Launch Screen storyboard, and related tests/docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
100
suixinkanTests/TravelAlbum/TravelAlbumAPITests.swift
Normal file
100
suixinkanTests/TravelAlbum/TravelAlbumAPITests.swift
Normal file
@ -0,0 +1,100 @@
|
||||
//
|
||||
// TravelAlbumAPITests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 旅拍相册 API 测试,覆盖 path、query 与 body。
|
||||
final class TravelAlbumAPITests: XCTestCase {
|
||||
/// 测试列表与创建接口 path。
|
||||
func testAlbumListAndCreateUseExpectedPaths() async throws {
|
||||
let session = TravelAlbumRecordingURLSession(responses: [Self.listResponse, Self.createResponse])
|
||||
let api = TravelAlbumAPI(client: APIClient(session: session))
|
||||
|
||||
let list = try await api.albumList(page: 1, pageSize: 20)
|
||||
_ = try await api.createAlbum(
|
||||
TravelAlbumCreateRequest(
|
||||
name: "2026-06-29-001",
|
||||
type: 1,
|
||||
orderNumber: nil,
|
||||
materialNum: 3,
|
||||
materialPrice: 9.9,
|
||||
materialPackagePrice: 99,
|
||||
photoPrice: 0
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||
"/api/yf-handset-app/photog/travel-album/list",
|
||||
"/api/yf-handset-app/photog/travel-album/create"
|
||||
])
|
||||
XCTAssertEqual(list.total, 1)
|
||||
XCTAssertEqual(list.list.first?.name, "2026-06-29-001")
|
||||
|
||||
let createBody = try bodyObject(from: session.requests[1])
|
||||
XCTAssertEqual(createBody["name"] as? String, "2026-06-29-001")
|
||||
XCTAssertEqual(createBody["type"] as? Int, 1)
|
||||
}
|
||||
|
||||
/// 测试上传素材登记接口 body。
|
||||
func testUploadMaterialUsesExpectedBody() async throws {
|
||||
let session = TravelAlbumRecordingURLSession(data: Self.uploadMaterialResponse)
|
||||
let api = TravelAlbumAPI(client: APIClient(session: session))
|
||||
|
||||
_ = try await api.uploadMaterial(
|
||||
TravelAlbumUploadMaterialRequest(
|
||||
userEquityTravelId: 8,
|
||||
fileName: "DSC_001.JPG",
|
||||
fileURL: "https://cdn/a.jpg"
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/travel-album/upload-material")
|
||||
let body = try bodyObject(from: try XCTUnwrap(session.requests.first))
|
||||
XCTAssertEqual(body["user_equity_travel_id"] as? Int, 8)
|
||||
XCTAssertEqual(body["file_name"] as? String, "DSC_001.JPG")
|
||||
XCTAssertEqual(body["file_url"] as? String, "https://cdn/a.jpg")
|
||||
}
|
||||
|
||||
fileprivate static let listResponse = Data(
|
||||
#"{"code":100000,"msg":"success","data":{"total":"1","list":[{"id":"8","store_user_id":"1","name":"2026-06-29-001","type":"1","order_number":"","material_num":"3","material_price":"990","material_package_price":"9900","photo_price":"0","cover_url":"","user_id":"100","status":"1","created_at":"2026","updated_at":"2026","user":{"id":"200","phone":"13800138000"}}]}}"#.utf8
|
||||
)
|
||||
|
||||
fileprivate static let createResponse = Data(
|
||||
#"{"code":100000,"msg":"success","data":{"id":"9"}}"#.utf8
|
||||
)
|
||||
|
||||
fileprivate static let uploadMaterialResponse = Data(
|
||||
#"{"code":100000,"msg":"success","data":{"id":"11","user_equity_travel_id":"8","status":"1","order_number":"","user_id":"100","file_name":"DSC_001.JPG","file_type":"2","file_url":"https://cdn/a.jpg","file_size":"2048","cover_url":"","is_purchased":"0","created_at":"2026","updated_at":"2026"}}"#.utf8
|
||||
)
|
||||
}
|
||||
|
||||
private final class TravelAlbumRecordingURLSession: URLSessionProtocol {
|
||||
private var responses: [Data]
|
||||
private(set) var requests: [URLRequest] = []
|
||||
|
||||
init(data: Data) {
|
||||
responses = [data]
|
||||
}
|
||||
|
||||
init(responses: [Data]) {
|
||||
self.responses = responses
|
||||
}
|
||||
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
requests.append(request)
|
||||
let data = responses.isEmpty ? TravelAlbumAPITests.listResponse : responses.removeFirst()
|
||||
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
return (data, response)
|
||||
}
|
||||
}
|
||||
|
||||
private func bodyObject(from request: URLRequest) throws -> [String: Any] {
|
||||
let data = try XCTUnwrap(request.httpBody)
|
||||
return try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
}
|
||||
124
suixinkanTests/TravelAlbum/TravelAlbumViewModelTests.swift
Normal file
124
suixinkanTests/TravelAlbum/TravelAlbumViewModelTests.swift
Normal file
@ -0,0 +1,124 @@
|
||||
//
|
||||
// TravelAlbumViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 旅拍相册 ViewModel 测试。
|
||||
final class TravelAlbumViewModelTests: XCTestCase {
|
||||
/// 测试先拍再买模式缺少单价时创建失败。
|
||||
func testCreatePreShootRequiresSinglePrice() async {
|
||||
let viewModel = TravelAlbumEntryViewModel()
|
||||
let api = MockTravelAlbumAPI()
|
||||
|
||||
let success = await viewModel.confirmCreateTask(
|
||||
api: api,
|
||||
mode: TravelAlbumEntryViewModel.modePreShoot,
|
||||
freeCount: "3",
|
||||
singlePrice: "",
|
||||
packagePrice: "99",
|
||||
order: nil,
|
||||
scenicSelected: true
|
||||
)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请输入单张照片价格")
|
||||
XCTAssertEqual(api.createCallCount, 0)
|
||||
}
|
||||
|
||||
/// 测试买了再拍模式缺少订单时创建失败。
|
||||
func testCreatePreOrderRequiresOrder() async {
|
||||
let viewModel = TravelAlbumEntryViewModel()
|
||||
let api = MockTravelAlbumAPI()
|
||||
|
||||
let success = await viewModel.confirmCreateTask(
|
||||
api: api,
|
||||
mode: TravelAlbumEntryViewModel.modePreOrder,
|
||||
freeCount: "",
|
||||
singlePrice: "",
|
||||
packagePrice: "",
|
||||
order: nil,
|
||||
scenicSelected: true
|
||||
)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请选择绑定订单")
|
||||
}
|
||||
|
||||
/// 测试创建成功后刷新列表。
|
||||
func testCreateSuccessReloadsList() async {
|
||||
let viewModel = TravelAlbumEntryViewModel()
|
||||
let api = MockTravelAlbumAPI()
|
||||
|
||||
let success = await viewModel.confirmCreateTask(
|
||||
api: api,
|
||||
mode: TravelAlbumEntryViewModel.modePreShoot,
|
||||
freeCount: "3",
|
||||
singlePrice: "9.9",
|
||||
packagePrice: "99",
|
||||
order: nil,
|
||||
scenicSelected: true
|
||||
)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(api.createCallCount, 1)
|
||||
XCTAssertEqual(api.listCallCount, 1)
|
||||
}
|
||||
|
||||
/// 测试相册名称按日期递增。
|
||||
func testBuildTodayAlbumNameIncrementsSuffix() {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let today = formatter.string(from: Date())
|
||||
|
||||
let name = TravelAlbumEntryViewModel.buildTodayAlbumName(
|
||||
existingNames: ["\(today)-001", "\(today)-002", "2026-06-28-001"]
|
||||
)
|
||||
XCTAssertTrue(name.hasSuffix("003"))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockTravelAlbumAPI: TravelAlbumServing {
|
||||
var createCallCount = 0
|
||||
var listCallCount = 0
|
||||
|
||||
func availableOrders() async throws -> [TravelAlbumAvailableOrder] { [] }
|
||||
|
||||
func createAlbum(_ request: TravelAlbumCreateRequest) async throws -> TravelAlbumCreateResponse {
|
||||
createCallCount += 1
|
||||
return TravelAlbumCreateResponse(id: 9)
|
||||
}
|
||||
|
||||
func albumList(page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumItem> {
|
||||
listCallCount += 1
|
||||
return ListPayload(total: 0, list: [])
|
||||
}
|
||||
|
||||
func albumInfo(id: Int) async throws -> TravelAlbumItem {
|
||||
throw APIError.emptyData
|
||||
}
|
||||
|
||||
func editAlbum(_ request: TravelAlbumEditRequest) async throws {}
|
||||
|
||||
func deleteAlbum(_ request: TravelAlbumDeleteRequest) async throws {}
|
||||
|
||||
func materialList(userEquityTravelId: Int, page: Int, pageSize: Int) async throws -> ListPayload<TravelAlbumMaterial> {
|
||||
ListPayload(total: 0, list: [])
|
||||
}
|
||||
|
||||
func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial {
|
||||
throw APIError.emptyData
|
||||
}
|
||||
|
||||
func deleteMaterial(_ request: TravelAlbumDeleteMaterialRequest) async throws {}
|
||||
|
||||
func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse {
|
||||
TravelAlbumMpCodeResponse(mpCodeOssURL: "")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user