FBSDKCoreKit/FBSDKCoreKitTests/ImageDownloaderTests.swift (254 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
@testable import FBSDKCoreKit
import TestTools
import XCTest
final class ImageDownloaderTests: XCTestCase {
let expectedCacheMemory = 1024 * 1024 * 8
let expectedCacheCapacity = 1024 * 1024 * 100
let defaultTTL = 60.0
let provider = TestSessionProvider()
lazy var downloader = ImageDownloader(sessionProvider: provider)
lazy var url = SampleURLs.valid(path: name)
lazy var request = URLRequest(url: url)
let (image, imageData): (UIImage, Data) = {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 36, height: 36), false, 1)
let image = UIGraphicsGetImageFromCurrentImageContext()! // swiftlint:disable:this force_unwrapping
UIGraphicsEndImageContext()
let imageData = image.pngData()! // swiftlint:disable:this force_unwrapping
return (image, imageData)
}()
override func setUp() {
super.setUp()
downloader.urlCache.removeAllCachedResponses()
}
func testDefaultSessionProvider() {
XCTAssertEqual(
ObjectIdentifier(ImageDownloader.sharedInstance.sessionProvider),
ObjectIdentifier(URLSession.shared),
"Should use the shared system session by default"
)
}
func testCreatingWithSession() {
XCTAssertEqual(
ObjectIdentifier(downloader.sessionProvider),
ObjectIdentifier(provider),
"Should be able to create with a session provider"
)
}
func testFetchingImageStartsTask() {
let dataTask = TestSessionDataTask()
provider.stubbedDataTask = dataTask
downloader.downloadImage(
with: url,
ttl: defaultTTL
) { _ in }
XCTAssertEqual(
provider.dataTaskCallCount,
1,
"Should request a data task when fetching an image"
)
XCTAssertEqual(
dataTask.resumeCallCount,
1,
"Should start the data task for fetching an image"
)
}
// Data | Response | Error
// nil | nil | nil
func testCompletingTaskWithMissingDataResponseAndError() {
completeFetchingImage(
data: nil,
response: nil,
error: nil,
message: "Should not complete with an image when the session task completes with no inputs"
)
}
// Data | Response | Error
// nil | nil | yes
func testCompletingTaskWithError() {
completeFetchingImage(
data: nil,
response: nil,
error: SampleError(),
message: "Should not complete with an image when the session task completes with an error"
)
}
// Data | Response | Error
// nil | yes (non-http) | nil
func testCompletingTaskWithNonHTTPResponse() {
completeFetchingImage(
data: nil,
response: SampleHTTPURLResponses.valid,
error: nil,
message: "Should not complete with an image when there is non-http response"
)
}
// Data | Response | Error
// nil | yes (http) | nil
func testCompletingTaskWithInvalidStatusCode() {
completeFetchingImage(
data: nil,
response: SampleHTTPURLResponses.invalidStatusCode,
error: nil,
message: "Should not complete with an image when there is an invalid url response status code"
)
}
// Data | Response | Error
// nil | yes | yes
func testCompletingTaskWithResponseAndError() {
completeFetchingImage(
data: nil,
response: SampleHTTPURLResponses.valid,
error: SampleError(),
message: "Should not complete with an image when there is an error"
)
}
// Data | Response | Error
// yes (invalid) | nil | nil
func testCompletingTaskWithInvalidData() {
completeFetchingImage(
data: SampleGraphResponses.empty.data,
response: nil,
error: nil,
message: "Should not complete with an image when there is invalid data"
)
}
// Data | Response | Error
// yes (valid) | nil | yes
func testCompletingTaskWithValidDataAndError() {
completeFetchingImage(
data: imageData,
response: nil,
error: SampleError(),
message: "Should not complete with an image when there is an error"
)
}
// Data | Response | Error
// yes (valid) | yes (invalid) | nil
func testCompletingTaskWithValidDataAndInvalidResponse() {
completeFetchingImage(
data: imageData,
response: SampleHTTPURLResponses.invalidStatusCode,
error: nil,
message: "Should not complete with an image when there is an invalid response code"
)
}
// Data | Response | Error
// yes (invalid) | yes (valid) | nil
func testCompletingTaskWithInvalidDataAndValidResponse() {
completeFetchingImage(
data: SampleGraphResponses.empty.data,
response: SampleHTTPURLResponses.validStatusCode,
error: nil,
message: "Should not complete with an image when there is invalid image data"
)
}
// Data | Response | Error
// yes (valid) | yes (valid) | nil
func testCompletingTaskWithValidDataAndValidResponse() {
completeFetchingImage(
data: imageData,
response: SampleHTTPURLResponses.validStatusCode,
error: nil,
expectedImage: image,
message: "Should not complete with an image when there is invalid image data"
)
}
// MARK: - Caching
func testDefaultCache() {
XCTAssertEqual(
downloader.urlCache.memoryCapacity,
expectedCacheMemory,
"Should use a well known value for the memory capacity of the image cache"
)
XCTAssertEqual(
downloader.urlCache.diskCapacity,
expectedCacheCapacity,
"Should use a well known value for the disk capacity of the image cache"
)
}
func testUncachedImagesAreFetched() {
downloader.downloadImage(with: url, ttl: defaultTTL, completion: nil)
XCTAssertEqual(
provider.dataTaskCallCount,
1,
"Should attempt to fetch an uncached image"
)
}
func testUnexpiredCachedImagesAreNotFetched() {
seedURLCache()
downloader.downloadImage(with: url, ttl: defaultTTL, completion: nil)
XCTAssertEqual(
provider.dataTaskCallCount,
0,
"Should not attempt to fetch a cached image"
)
}
func testExpiredCachedImagedAreFetched() {
seedURLCache(date: .distantPast)
downloader.downloadImage(with: url, ttl: defaultTTL, completion: nil)
XCTAssertEqual(
provider.dataTaskCallCount,
1,
"Should attempt to fetch a cached image if it is expired"
)
}
func testFetchingCachedImage() {
seedURLCache()
var completionInvoked = false
downloader.downloadImage(with: url, ttl: defaultTTL) { potentialImage in
guard let image = potentialImage else {
return XCTFail("Should call the fetch completion with an image created from cached image data")
}
XCTAssertEqual(
image.pngData(),
self.imageData,
"Should call the fetch completion with an image created from cached image data"
)
completionInvoked = true
}
provider.capturedCompletion?(nil, nil, nil)
XCTAssertTrue(completionInvoked)
}
func testFetchingWithSuccessSavesToCache() {
let response = SampleHTTPURLResponses.validStatusCode
downloader.downloadImage(with: url, ttl: defaultTTL) { _ in }
provider.capturedCompletion?(imageData, response, nil)
let expectedCachedResponse = CachedURLResponse(response: response, data: imageData)
guard let cachedResponse = downloader.urlCache.cachedResponse(for: request) else {
return XCTFail("Should have a cached response")
}
XCTAssertEqual(
cachedResponse.data,
expectedCachedResponse.data,
"Should cache a successful fetch"
)
}
func testClearingCache() {
seedURLCache()
downloader.removeAll()
XCTAssertNil(downloader.urlCache.cachedResponse(for: request))
}
// MARK: - Helpers
func seedURLCache(date: Date = Date()) {
downloader.urlCache.storeCachedResponse(
CachedURLResponse(
response: SampleHTTPURLResponses.validStatusCode,
data: imageData,
userInfo: ["timestamp": date],
storagePolicy: URLCache.StoragePolicy.allowed
),
for: request
)
XCTAssertNotNil(downloader.urlCache.cachedResponse(for: request))
}
func completeFetchingImage(
data: Data?,
response: URLResponse?,
error: Error?,
expectedImage: UIImage? = nil,
message: String,
file: StaticString = #file,
line: UInt = #line
) {
var completionInvoked = false
var image: UIImage?
downloader.downloadImage(
with: SampleURLs.valid,
ttl: 0
) { potentialImage in
image = potentialImage
completionInvoked = true
}
provider.capturedCompletion?(data, response, error)
XCTAssertEqual(
image?.pngData(),
expectedImage?.pngData(),
message,
file: file,
line: line
)
XCTAssertTrue(
completionInvoked,
file: file,
line: line
)
}
}