Implement remote library metadata sync for iPhone
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user