618 lines
20 KiB
Swift
618 lines
20 KiB
Swift
import Foundation
|
|
import Flutter
|
|
import XCTest
|
|
|
|
@testable import Runner
|
|
|
|
private final class ShelfArtworkURLProtocol: URLProtocol {
|
|
private static let lock = NSLock()
|
|
private static var startHandler: ((ShelfArtworkURLProtocol) -> Void)?
|
|
private static var stopHandler: (() -> Void)?
|
|
|
|
static func configure(
|
|
start: @escaping (ShelfArtworkURLProtocol) -> Void,
|
|
stop: (() -> Void)? = nil
|
|
) {
|
|
lock.lock()
|
|
startHandler = start
|
|
stopHandler = stop
|
|
lock.unlock()
|
|
}
|
|
|
|
static func reset() {
|
|
lock.lock()
|
|
startHandler = nil
|
|
stopHandler = nil
|
|
lock.unlock()
|
|
}
|
|
|
|
override class func canInit(with request: URLRequest) -> Bool { true }
|
|
|
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
|
|
|
override func startLoading() {
|
|
Self.lock.lock()
|
|
let handler = Self.startHandler
|
|
Self.lock.unlock()
|
|
guard let handler else {
|
|
client?.urlProtocol(self, didFailWithError: URLError(.resourceUnavailable))
|
|
return
|
|
}
|
|
handler(self)
|
|
}
|
|
|
|
override func stopLoading() {
|
|
Self.lock.lock()
|
|
let handler = Self.stopHandler
|
|
Self.lock.unlock()
|
|
handler?()
|
|
}
|
|
|
|
func sendResponse(contentLength: Int? = nil) {
|
|
var headers = ["Content-Type": "image/png"]
|
|
if let contentLength { headers["Content-Length"] = String(contentLength) }
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 200,
|
|
httpVersion: "HTTP/1.1",
|
|
headerFields: headers
|
|
)!
|
|
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
}
|
|
|
|
func send(_ data: Data) {
|
|
client?.urlProtocol(self, didLoad: data)
|
|
}
|
|
|
|
func finish() {
|
|
client?.urlProtocolDidFinishLoading(self)
|
|
}
|
|
}
|
|
|
|
private final class SystemShelfSyncHarness {
|
|
let defaults: UserDefaults
|
|
let root: URL
|
|
private(set) var state = SystemShelfMutationState()
|
|
private(set) var epoch: UInt64 = 0
|
|
|
|
private struct ScheduledPrune {
|
|
let deadline: TimeInterval
|
|
let batch: SystemShelfPruneBatch
|
|
}
|
|
|
|
private let suiteName: String
|
|
private var pruneClock: TimeInterval = 0
|
|
private var scheduledPrunes: [ScheduledPrune] = []
|
|
|
|
var now: () -> Date = { Date(timeIntervalSince1970: 1_000) }
|
|
var loaderShouldFail = false
|
|
private(set) var requestedURLs: [URL] = []
|
|
private(set) var notificationCount = 0
|
|
|
|
init() throws {
|
|
suiteName = "SystemShelfSyncHarness.\(UUID().uuidString)"
|
|
defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
|
|
root = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
epoch = state.beginEngineSession()
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
}
|
|
|
|
func cleanup() {
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
try? FileManager.default.removeItem(at: root)
|
|
}
|
|
|
|
@discardableResult
|
|
func beginEngineSession() -> UInt64 {
|
|
epoch = state.beginEngineSession()
|
|
return epoch
|
|
}
|
|
|
|
func sync(
|
|
generation: Int64,
|
|
items: [[String: Any]],
|
|
ownerId: String = "profile-a",
|
|
engineEpoch: UInt64? = nil
|
|
) -> Bool {
|
|
let environment = SystemShelfSyncEnvironment(
|
|
defaults: defaults,
|
|
artworkRoot: root,
|
|
now: now,
|
|
loadArtwork: { [unowned self] url, _, _ in
|
|
requestedURLs.append(url)
|
|
guard !loaderShouldFail else { return nil }
|
|
return BoundedArtworkDownload(data: Self.png, mimeType: "image/png")
|
|
},
|
|
schedulePrune: { [unowned self] batches in
|
|
scheduledPrunes.append(
|
|
contentsOf: batches.map {
|
|
ScheduledPrune(deadline: pruneClock + $0.delay, batch: $0)
|
|
}
|
|
)
|
|
},
|
|
notifyChange: { [unowned self] in
|
|
notificationCount += 1
|
|
}
|
|
)
|
|
return SystemShelfPlugin.sync(
|
|
envelope: envelope(
|
|
generation: generation,
|
|
ownerId: ownerId,
|
|
engineEpoch: engineEpoch
|
|
),
|
|
rawItems: items,
|
|
state: &state,
|
|
environment: environment
|
|
)
|
|
}
|
|
|
|
func clear(generation: Int64, ownerId: String = "profile-a") -> Bool {
|
|
SystemShelfPlugin.clearCache(
|
|
envelope: envelope(generation: generation, ownerId: ownerId),
|
|
state: &state,
|
|
defaults: defaults,
|
|
artworkRoot: root,
|
|
notifyChange: { [unowned self] in
|
|
notificationCount += 1
|
|
}
|
|
)
|
|
}
|
|
|
|
func remove(generation: Int64, contentId: String) -> Bool {
|
|
SystemShelfPlugin.removeItem(
|
|
envelope: envelope(generation: generation),
|
|
contentId: contentId,
|
|
state: &state,
|
|
defaults: defaults,
|
|
artworkRoot: root,
|
|
schedulePrune: { [unowned self] batches in
|
|
scheduledPrunes.append(
|
|
contentsOf: batches.map {
|
|
ScheduledPrune(deadline: pruneClock + $0.delay, batch: $0)
|
|
}
|
|
)
|
|
},
|
|
notifyChange: { [unowned self] in
|
|
notificationCount += 1
|
|
}
|
|
)
|
|
}
|
|
|
|
func envelope(
|
|
generation: Int64,
|
|
ownerId: String = "profile-a",
|
|
engineEpoch: UInt64? = nil
|
|
) -> SystemShelfMutationEnvelope {
|
|
SystemShelfMutationEnvelope(
|
|
ownerId: ownerId,
|
|
generation: generation,
|
|
engineEpoch: engineEpoch ?? epoch
|
|
)
|
|
}
|
|
|
|
func currentItems() throws -> [[String: Any]] {
|
|
let data = try XCTUnwrap(defaults.data(forKey: SystemShelfPlugin.cacheDataKey))
|
|
let payload = try XCTUnwrap(
|
|
JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
)
|
|
let sections = try XCTUnwrap(payload["sections"] as? [[String: Any]])
|
|
return sections.flatMap { $0["items"] as? [[String: Any]] ?? [] }
|
|
}
|
|
|
|
func artworkURL(for key: String) -> URL? {
|
|
guard
|
|
let enumerator = FileManager.default.enumerator(
|
|
at: root,
|
|
includingPropertiesForKeys: nil
|
|
)
|
|
else { return nil }
|
|
return enumerator.compactMap { $0 as? URL }.first { $0.lastPathComponent == key }
|
|
}
|
|
|
|
func advancePrunes(by interval: TimeInterval) {
|
|
pruneClock += interval
|
|
let due = scheduledPrunes.filter { $0.deadline <= pruneClock }
|
|
scheduledPrunes.removeAll { $0.deadline <= pruneClock }
|
|
for scheduled in due {
|
|
let candidates = state.claimPruning(scheduled.batch)
|
|
SystemShelfPlugin.pruneUnreferenced(
|
|
candidates,
|
|
defaults: defaults,
|
|
root: root
|
|
)
|
|
}
|
|
}
|
|
|
|
static func item(
|
|
source: String? = nil,
|
|
progress: Int = 0,
|
|
contentId: String = "movie-1"
|
|
) -> [String: Any] {
|
|
var item: [String: Any] = [
|
|
"contentId": contentId,
|
|
"title": "Movie",
|
|
"lastPlaybackPosition": progress,
|
|
]
|
|
if let source {
|
|
item["posterSourceUri"] = source
|
|
}
|
|
return item
|
|
}
|
|
|
|
static let png = Data(
|
|
base64Encoded:
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
|
)!
|
|
}
|
|
|
|
final class SystemShelfPluginTests: XCTestCase {
|
|
override func tearDown() {
|
|
ShelfArtworkURLProtocol.reset()
|
|
super.tearDown()
|
|
}
|
|
|
|
func testSyncRemoveAndClearUpdateCommittedShelf() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [
|
|
SystemShelfSyncHarness.item(
|
|
source: "https://shelf.test/a.png",
|
|
contentId: "movie-a"
|
|
),
|
|
SystemShelfSyncHarness.item(
|
|
source: "https://shelf.test/b.png",
|
|
contentId: "movie-b"
|
|
),
|
|
]
|
|
)
|
|
)
|
|
let initialItems = try harness.currentItems()
|
|
XCTAssertEqual(Set(initialItems.compactMap { $0["contentId"] as? String }), ["movie-a", "movie-b"])
|
|
let removedKey = try XCTUnwrap(
|
|
initialItems.first { $0["contentId"] as? String == "movie-a" }?["artworkKey"] as? String
|
|
)
|
|
let removedArtwork = try XCTUnwrap(harness.artworkURL(for: removedKey))
|
|
|
|
XCTAssertTrue(harness.remove(generation: 2, contentId: "movie-a"))
|
|
XCTAssertEqual(try harness.currentItems().compactMap { $0["contentId"] as? String }, ["movie-b"])
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: removedArtwork.path))
|
|
harness.advancePrunes(by: 60)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: removedArtwork.path))
|
|
|
|
XCTAssertTrue(harness.clear(generation: 3))
|
|
XCTAssertNil(harness.defaults.data(forKey: SystemShelfPlugin.cacheDataKey))
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: harness.root.path))
|
|
XCTAssertEqual(harness.notificationCount, 3)
|
|
}
|
|
|
|
func testReplacementEngineRejectsStaleEngineMutation() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
let staleEpoch = harness.epoch
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 50,
|
|
items: [SystemShelfSyncHarness.item(progress: 50)]
|
|
)
|
|
)
|
|
_ = harness.beginEngineSession()
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [SystemShelfSyncHarness.item(progress: 1)],
|
|
ownerId: "profile-b"
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(
|
|
harness.sync(
|
|
generation: 51,
|
|
items: [SystemShelfSyncHarness.item(progress: 99)],
|
|
engineEpoch: staleEpoch
|
|
)
|
|
)
|
|
let item = try XCTUnwrap(harness.currentItems().first)
|
|
XCTAssertEqual(item["lastPlaybackPosition"] as? Int, 1)
|
|
let data = try XCTUnwrap(
|
|
harness.defaults.data(forKey: SystemShelfPlugin.cacheDataKey)
|
|
)
|
|
let payload = try XCTUnwrap(
|
|
JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
)
|
|
XCTAssertEqual(payload["ownerId"] as? String, "profile-b")
|
|
}
|
|
|
|
func testHigherGenerationRemainsLastWriter() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 2,
|
|
items: [SystemShelfSyncHarness.item(progress: 20)]
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [SystemShelfSyncHarness.item(progress: 10)]
|
|
)
|
|
)
|
|
XCTAssertEqual(
|
|
try harness.currentItems().first?["lastPlaybackPosition"] as? Int,
|
|
20
|
|
)
|
|
XCTAssertEqual(harness.notificationCount, 1)
|
|
}
|
|
|
|
func testProgressOnlySyncReusesCommittedArtworkWithoutRequestingItAgain() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
let source = "https://shelf.test/movie-a.png"
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [SystemShelfSyncHarness.item(source: source, progress: 10)]
|
|
)
|
|
)
|
|
let firstKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let firstArtwork = try XCTUnwrap(harness.artworkURL(for: firstKey))
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 2,
|
|
items: [SystemShelfSyncHarness.item(source: source, progress: 45)]
|
|
)
|
|
)
|
|
let updatedItem = try XCTUnwrap(harness.currentItems().first)
|
|
XCTAssertEqual(updatedItem["lastPlaybackPosition"] as? Int, 45)
|
|
XCTAssertEqual(updatedItem["artworkKey"] as? String, firstKey)
|
|
XCTAssertEqual(harness.requestedURLs.map(\.absoluteString), [source])
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstArtwork.path))
|
|
}
|
|
|
|
func testFailedReplacementKeepsLastCommittedArtwork() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
let firstSource = "https://shelf.test/movie-a.png"
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [SystemShelfSyncHarness.item(source: firstSource)]
|
|
)
|
|
)
|
|
let firstKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let firstArtwork = try XCTUnwrap(harness.artworkURL(for: firstKey))
|
|
|
|
harness.loaderShouldFail = true
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 2,
|
|
items: [
|
|
SystemShelfSyncHarness.item(source: "https://shelf.test/movie-b.png", progress: 20)
|
|
]
|
|
)
|
|
)
|
|
|
|
let item = try XCTUnwrap(harness.currentItems().first)
|
|
XCTAssertEqual(item["artworkKey"] as? String, firstKey)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstArtwork.path))
|
|
XCTAssertEqual(
|
|
harness.requestedURLs.map(\.absoluteString),
|
|
[firstSource, "https://shelf.test/movie-b.png"]
|
|
)
|
|
}
|
|
|
|
func testEachSupersededArtworkReceivesFullPruneGrace() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [SystemShelfSyncHarness.item(source: "https://shelf.test/a.png")]
|
|
)
|
|
)
|
|
let firstKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let firstArtwork = try XCTUnwrap(harness.artworkURL(for: firstKey))
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 2,
|
|
items: [SystemShelfSyncHarness.item(source: "https://shelf.test/b.png")]
|
|
)
|
|
)
|
|
let secondKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let secondArtwork = try XCTUnwrap(harness.artworkURL(for: secondKey))
|
|
|
|
harness.advancePrunes(by: 59)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstArtwork.path))
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 3,
|
|
items: [SystemShelfSyncHarness.item(source: "https://shelf.test/c.png")]
|
|
)
|
|
)
|
|
let currentKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let currentArtwork = try XCTUnwrap(harness.artworkURL(for: currentKey))
|
|
|
|
harness.advancePrunes(by: 1)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: firstArtwork.path))
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: secondArtwork.path))
|
|
harness.advancePrunes(by: 58)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: secondArtwork.path))
|
|
harness.advancePrunes(by: 1)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: secondArtwork.path))
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: currentArtwork.path))
|
|
}
|
|
|
|
func testPruneRechecksCurrentArtworkAndRelaunchRecoveryPreservesIt() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 1,
|
|
items: [SystemShelfSyncHarness.item(source: "https://shelf.test/a.png")]
|
|
)
|
|
)
|
|
let firstKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let firstArtwork = try XCTUnwrap(harness.artworkURL(for: firstKey))
|
|
XCTAssertTrue(
|
|
harness.sync(
|
|
generation: 2,
|
|
items: [SystemShelfSyncHarness.item(source: "https://shelf.test/b.png")]
|
|
)
|
|
)
|
|
let secondKey = try XCTUnwrap(harness.currentItems().first?["artworkKey"] as? String)
|
|
let secondArtwork = try XCTUnwrap(harness.artworkURL(for: secondKey))
|
|
|
|
try replaceCommittedArtworkKey(
|
|
in: harness.defaults,
|
|
with: firstKey
|
|
)
|
|
harness.advancePrunes(by: 60)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstArtwork.path))
|
|
|
|
let relaunchedAt = Date(timeIntervalSince1970: 10_000)
|
|
let recovered = SystemShelfPlugin.recoverableArtwork(
|
|
root: harness.root,
|
|
keeping: [firstArtwork],
|
|
now: relaunchedAt
|
|
)
|
|
XCTAssertEqual(
|
|
recovered[secondArtwork],
|
|
relaunchedAt.addingTimeInterval(60)
|
|
)
|
|
XCTAssertNil(recovered[firstArtwork])
|
|
|
|
var relaunchedState = SystemShelfMutationState()
|
|
_ = relaunchedState.beginEngineSession()
|
|
XCTAssertNotNil(relaunchedState.adoptPersistedOwner("profile-a"))
|
|
relaunchedState.cancelPruning(keeping: [firstArtwork])
|
|
let batches = relaunchedState.preparePruning(
|
|
removing: Set(recovered.keys),
|
|
after: 60
|
|
)
|
|
XCTAssertEqual(batches.count, 1)
|
|
XCTAssertEqual(batches.first?.delay, 60)
|
|
let candidates = relaunchedState.claimPruning(try XCTUnwrap(batches.first))
|
|
SystemShelfPlugin.pruneUnreferenced(
|
|
candidates,
|
|
defaults: harness.defaults,
|
|
root: harness.root
|
|
)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: secondArtwork.path))
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: firstArtwork.path))
|
|
}
|
|
|
|
func testPruneDoesNotEscapeArtworkRoot() throws {
|
|
let harness = try SystemShelfSyncHarness()
|
|
defer { harness.cleanup() }
|
|
let outside = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("\(UUID().uuidString).art")
|
|
let inside = harness.root.appendingPathComponent("orphan.art")
|
|
let outsideDirectory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let outsideViaSymlink = outsideDirectory.appendingPathComponent("linked.art")
|
|
let linkedDirectory = harness.root.appendingPathComponent("linked", isDirectory: true)
|
|
let linkedCandidate = linkedDirectory.appendingPathComponent("linked.art")
|
|
try Data([0x01]).write(to: outside)
|
|
try Data([0x02]).write(to: inside)
|
|
try FileManager.default.createDirectory(
|
|
at: outsideDirectory,
|
|
withIntermediateDirectories: true
|
|
)
|
|
try Data([0x03]).write(to: outsideViaSymlink)
|
|
try FileManager.default.createSymbolicLink(
|
|
at: linkedDirectory,
|
|
withDestinationURL: outsideDirectory
|
|
)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: outside)
|
|
try? FileManager.default.removeItem(at: outsideDirectory)
|
|
}
|
|
|
|
SystemShelfPlugin.pruneUnreferenced(
|
|
[outside, inside, linkedCandidate],
|
|
defaults: harness.defaults,
|
|
root: harness.root
|
|
)
|
|
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: outside.path))
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: outsideViaSymlink.path))
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: inside.path))
|
|
}
|
|
|
|
func testArtworkLoaderCancelsUnknownLengthBodyAtConfiguredCap() {
|
|
let stopped = expectation(description: "oversized request cancelled")
|
|
ShelfArtworkURLProtocol.configure(
|
|
start: { request in
|
|
request.sendResponse()
|
|
request.send(Data(repeating: 0x41, count: 5))
|
|
request.send(Data(repeating: 0x42, count: 5))
|
|
request.finish()
|
|
},
|
|
stop: { stopped.fulfill() }
|
|
)
|
|
let configuration = URLSessionConfiguration.ephemeral
|
|
configuration.protocolClasses = [ShelfArtworkURLProtocol.self]
|
|
let loader = BoundedArtworkLoader(maximumBytes: 8, configuration: configuration)
|
|
|
|
let result = loader.load(url: URL(string: "https://shelf.test/art.png")!, timeout: 1)
|
|
|
|
XCTAssertNil(result)
|
|
XCTAssertTrue(loader.exceededLimit)
|
|
XCTAssertLessThanOrEqual(loader.peakBufferedBytes, 8)
|
|
wait(for: [stopped], timeout: 1)
|
|
}
|
|
|
|
func testArtworkLoaderReturnsBoundedChunkedBodyAndMimeType() {
|
|
let expected = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07])
|
|
ShelfArtworkURLProtocol.configure { request in
|
|
request.sendResponse()
|
|
request.send(expected.prefix(3))
|
|
request.send(expected.suffix(4))
|
|
request.finish()
|
|
}
|
|
let configuration = URLSessionConfiguration.ephemeral
|
|
configuration.protocolClasses = [ShelfArtworkURLProtocol.self]
|
|
let loader = BoundedArtworkLoader(maximumBytes: 8, configuration: configuration)
|
|
|
|
let result = loader.load(url: URL(string: "https://shelf.test/art.png")!, timeout: 1)
|
|
|
|
XCTAssertEqual(result?.data, expected)
|
|
XCTAssertEqual(result?.mimeType, "image/png")
|
|
XCTAssertEqual(loader.peakBufferedBytes, expected.count)
|
|
XCTAssertFalse(loader.exceededLimit)
|
|
}
|
|
|
|
private func replaceCommittedArtworkKey(
|
|
in defaults: UserDefaults,
|
|
with key: String
|
|
) throws {
|
|
let data = try XCTUnwrap(
|
|
defaults.data(forKey: SystemShelfPlugin.cacheDataKey)
|
|
)
|
|
var payload = try XCTUnwrap(
|
|
JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
)
|
|
var sections = try XCTUnwrap(payload["sections"] as? [[String: Any]])
|
|
var items = try XCTUnwrap(sections.first?["items"] as? [[String: Any]])
|
|
items[0]["artworkKey"] = key
|
|
sections[0]["items"] = items
|
|
payload["sections"] = sections
|
|
defaults.set(
|
|
try JSONSerialization.data(withJSONObject: payload),
|
|
forKey: SystemShelfPlugin.cacheDataKey
|
|
)
|
|
}
|
|
}
|