velody/packages/apple/VelodySync/Tests/VelodySyncTests/RemoteLibrarySyncServiceTests.swift

217 lines
6.8 KiB
Swift

import Foundation
import XCTest
import VelodyDomain
import VelodyNetworking
import VelodyPersistence
@testable import VelodySync
final class RemoteLibrarySyncServiceTests: XCTestCase {
func testSuccessfulSyncPersistsRemoteTracks() async throws {
let store = InMemoryRemoteLibraryStore()
let service = RemoteLibrarySyncService(
repository: DefaultRemoteLibraryRepository(
apiClient: MockVelodyAPIClient(
remoteLibraryResponse: RemoteLibraryResponseDTO(
tracks: [
RemoteTrackDTO(
trackId: "track-123",
title: "Remote Title",
artist: "Remote Artist",
durationSeconds: 245,
sha256: String(repeating: "a", count: 64),
assetId: "asset-456",
createdAt: "2026-05-29T08:00:00.000Z",
updatedAt: "2026-05-29T08:05:00.000Z"
),
]
)
),
store: store
)
)
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
let cachedTracks = try await service.loadCachedRemoteTracks()
XCTAssertEqual(tracks.count, 1)
XCTAssertEqual(cachedTracks, tracks)
XCTAssertEqual(cachedTracks.first?.trackId, "track-123")
}
func testEmptyResponseClearsCachedRemoteLibrary() async throws {
let store = InMemoryRemoteLibraryStore(
tracks: [
RemoteTrack(
trackId: "track-123",
title: "Old",
artist: "Artist",
durationSeconds: 100,
sha256: String(repeating: "b", count: 64),
assetId: "asset-123",
createdAt: "2026-05-29T08:00:00.000Z",
updatedAt: "2026-05-29T08:05:00.000Z"
),
]
)
let service = RemoteLibrarySyncService(
repository: DefaultRemoteLibraryRepository(
apiClient: MockVelodyAPIClient(
remoteLibraryResponse: RemoteLibraryResponseDTO(tracks: [])
),
store: store
)
)
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
let cachedTracks = try await service.loadCachedRemoteTracks()
XCTAssertEqual(tracks, [])
XCTAssertEqual(cachedTracks, [])
}
func testNetworkFailureLeavesCachedRemoteLibraryIntact() async throws {
let cachedTrack = RemoteTrack(
trackId: "track-123",
title: "Cached",
artist: "Artist",
durationSeconds: 100,
sha256: String(repeating: "c", count: 64),
assetId: "asset-123",
createdAt: "2026-05-29T08:00:00.000Z",
updatedAt: "2026-05-29T08:05:00.000Z"
)
let store = InMemoryRemoteLibraryStore(tracks: [cachedTrack])
let service = RemoteLibrarySyncService(
repository: DefaultRemoteLibraryRepository(
apiClient: MockVelodyAPIClient(
remoteLibraryError: VelodyAPIError.requestFailed("Offline")
),
store: store
)
)
await XCTAssertThrowsErrorAsync {
_ = try await service.syncRemoteLibrary(deviceId: "device-123")
}
let cachedTracks = try await service.loadCachedRemoteTracks()
XCTAssertEqual(cachedTracks, [cachedTrack])
}
}
private struct MockVelodyAPIClient: VelodyAPIClient {
let remoteLibraryResponse: RemoteLibraryResponseDTO?
let remoteLibraryError: VelodyAPIError?
init(
remoteLibraryResponse: RemoteLibraryResponseDTO? = nil,
remoteLibraryError: VelodyAPIError? = nil
) {
self.remoteLibraryResponse = remoteLibraryResponse
self.remoteLibraryError = remoteLibraryError
}
func registerDevice(
_ payload: DeviceRegistrationPayload
) async throws -> DeviceRegistrationResponse {
_ = payload
return DeviceRegistrationResponse(
deviceId: UUID().uuidString,
bootstrapToken: UUID().uuidString,
serverTime: "2026-05-29T08:00:00.000Z"
)
}
func sendHeartbeat(
_ payload: DeviceHeartbeatPayload
) async throws -> DeviceHeartbeatResponse {
_ = payload
return DeviceHeartbeatResponse(
ok: true,
serverTime: "2026-05-29T08:00:00.000Z"
)
}
func fetchSyncBootstrap() async throws -> SyncBootstrapResponse {
SyncBootstrapResponse(
nextCursor: SyncCursor(value: "0"),
tracks: [],
events: [],
deletedTrackIds: [],
serverTime: "2026-05-29T08:00:00.000Z"
)
}
func fetchRemoteLibrary(
deviceId: String
) async throws -> RemoteLibraryResponseDTO {
_ = deviceId
if let remoteLibraryError {
throw remoteLibraryError
}
return remoteLibraryResponse ?? RemoteLibraryResponseDTO(tracks: [])
}
func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse {
_ = payload
return UploadPrepareResponse(status: .uploadRequired, uploadId: UUID().uuidString, nextOffset: 0)
}
func fetchUploadStatus(
uploadId: String
) async throws -> UploadSessionStatusResponse {
UploadSessionStatusResponse(
uploadId: uploadId,
status: .completed,
receivedBytes: "0",
expectedSizeBytes: "0",
nextOffset: "0"
)
}
func uploadFile(
uploadId: String,
fileURL: URL,
mimeType: String
) async throws -> UploadSessionStatusResponse {
_ = fileURL
_ = mimeType
return UploadSessionStatusResponse(
uploadId: uploadId,
status: .completed,
receivedBytes: "0",
expectedSizeBytes: "0",
nextOffset: "0"
)
}
func finalizeUpload(
uploadId: String,
payload: UploadFinalizeRequest
) async throws -> UploadFinalizeResponse {
_ = uploadId
_ = payload
return UploadFinalizeResponse(
trackId: UUID().uuidString,
assetId: UUID().uuidString
)
}
}
private func XCTAssertThrowsErrorAsync(
_ expression: @escaping () async throws -> Void,
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
try await expression()
XCTFail("Expected expression to throw an error.", file: file, line: line)
} catch {
XCTAssertTrue(true)
}
}