Implement Milestone 7.2 offline audio downloads
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import VelodyPersistence
|
||||
|
||||
final class OfflineAudioFileStoreTests: XCTestCase {
|
||||
func testFileOfflineAudioFileStoreWritesAndReadsAudioData() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let store = try FileOfflineAudioFileStore(baseDirectoryURL: tempDirectory)
|
||||
let bytes = sampleMp3Data(seed: "offline-audio")
|
||||
|
||||
let localFilePath = try await store.saveAudioFile(
|
||||
bytes,
|
||||
assetId: "asset-123",
|
||||
sha256: sha256Hex(bytes)
|
||||
)
|
||||
let storedBytes = try await store.readAudioFile(at: localFilePath)
|
||||
let fileExists = await store.fileExists(at: localFilePath)
|
||||
|
||||
XCTAssertEqual(storedBytes, bytes)
|
||||
XCTAssertTrue(fileExists)
|
||||
}
|
||||
|
||||
func testFileOfflineAudioFileStoreRejectsEmptyAudioData() async throws {
|
||||
let store = try FileOfflineAudioFileStore(
|
||||
baseDirectoryURL: FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
)
|
||||
|
||||
await XCTAssertThrowsErrorAsync {
|
||||
_ = try await store.saveAudioFile(
|
||||
Data(),
|
||||
assetId: "asset-123",
|
||||
sha256: nil
|
||||
)
|
||||
} assertion: { error in
|
||||
XCTAssertEqual(error as? OfflineAudioFileStoreError, .emptyAudioData)
|
||||
}
|
||||
}
|
||||
|
||||
func testFileOfflineAudioFileStoreRejectsShaMismatch() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let store = try FileOfflineAudioFileStore(baseDirectoryURL: tempDirectory)
|
||||
let bytes = sampleMp3Data(seed: "sha-mismatch")
|
||||
|
||||
await XCTAssertThrowsErrorAsync {
|
||||
_ = try await store.saveAudioFile(
|
||||
bytes,
|
||||
assetId: "asset-123",
|
||||
sha256: String(repeating: "f", count: 64)
|
||||
)
|
||||
} assertion: { error in
|
||||
guard case .sha256Mismatch = error as? OfflineAudioFileStoreError else {
|
||||
return XCTFail("Expected a sha256Mismatch error.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFileOfflineAudioFileStoreResolvesCurrentBaseDirectoryWhenPersistedPathIsStale() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
let firstAudioDirectory = tempDirectory.appendingPathComponent("audio-v1", isDirectory: true)
|
||||
let secondAudioDirectory = tempDirectory.appendingPathComponent("audio-v2", isDirectory: true)
|
||||
let bytes = sampleMp3Data(seed: "path-repair")
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let staleFilePath = firstAudioDirectory
|
||||
.appendingPathComponent("asset-123.mp3")
|
||||
.standardizedFileURL
|
||||
.path
|
||||
let secondStore = try FileOfflineAudioFileStore(baseDirectoryURL: secondAudioDirectory)
|
||||
let currentFilePath = try await secondStore.saveAudioFile(
|
||||
bytes,
|
||||
assetId: "asset-123",
|
||||
sha256: sha256Hex(bytes)
|
||||
)
|
||||
|
||||
let resolvedFilePath = await secondStore.resolveLocalFilePath(
|
||||
persistedLocalFilePath: staleFilePath,
|
||||
assetId: "asset-123"
|
||||
)
|
||||
|
||||
XCTAssertEqual(resolvedFilePath, currentFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
private func sampleMp3Data(seed: String) -> Data {
|
||||
Data([
|
||||
0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21,
|
||||
] + Array(seed.utf8))
|
||||
}
|
||||
|
||||
private func sha256Hex(_ data: Data) -> String {
|
||||
SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
private func XCTAssertThrowsErrorAsync(
|
||||
_ expression: @escaping () async throws -> Void,
|
||||
assertion: (Error) -> Void,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await expression()
|
||||
XCTFail("Expected expression to throw an error.", file: file, line: line)
|
||||
} catch {
|
||||
assertion(error)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import VelodyDomain
|
||||
@testable import VelodyPersistence
|
||||
|
||||
final class RemoteTrackDownloadStateStoreTests: XCTestCase {
|
||||
func testFileDownloadStateStorePersistsAcrossInstances() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
let fileURL = tempDirectory.appendingPathComponent("remote-download-states.json")
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let firstStore = try FileRemoteTrackDownloadStateStore(fileURL: fileURL)
|
||||
let states = [
|
||||
RemoteTrackDownloadState(
|
||||
remoteTrackId: "track-123",
|
||||
assetId: "asset-456",
|
||||
localFilePath: "/tmp/asset-456.mp3",
|
||||
downloadedAt: Date(timeIntervalSince1970: 1_000),
|
||||
downloadStatus: .downloaded,
|
||||
lastDownloadError: nil
|
||||
),
|
||||
]
|
||||
|
||||
try await firstStore.saveDownloadStates(states)
|
||||
|
||||
let secondStore = try FileRemoteTrackDownloadStateStore(fileURL: fileURL)
|
||||
let restoredStates = try await secondStore.loadDownloadStates()
|
||||
|
||||
XCTAssertEqual(restoredStates, states)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user