Skip to content

Instantly share code, notes, and snippets.

@spicyjack
Created March 20, 2023 16:00
Show Gist options
  • Save spicyjack/75fefd4c551607f62ae4ddd4dfdc9f6c to your computer and use it in GitHub Desktop.
Save spicyjack/75fefd4c551607f62ae4ddd4dfdc9f6c to your computer and use it in GitHub Desktop.
URL shortening tests using URLSession with both closures and async/await
//
// BitlyURLShortenerTests.swift
//
// Test URL shortening code
//
//
import XCTVapor
@testable import App
class BitlyURLShortenerTests: XCTestCase {
private var testBitly: BitlyURLShortener!
private var spyURLSession: SpyURLSession!
override func setUpWithError() throws {
guard let bitlyAPIToken = Environment.get("BITLY_API_TOKEN") else {
throw ConfigError.missingBitlyAPIToken(msg: "ERROR: missing bit.ly API token in environment")
}
guard let bitlyGroupGUID = Environment.get("BITLY_GROUP_GUID") else {
throw ConfigError.missingBitlyGroupGUID(msg: "ERROR: missing bit.ly group GUID in environment")
}
spyURLSession = SpyURLSession()
testBitly = BitlyURLShortener(session: spyURLSession,
token: bitlyAPIToken,
groupGUID: bitlyGroupGUID)
}
func test_shorten_successfulBitlyResponse() throws {
let testURL = URL(string: "http://example.com/foo")!
var testBitlyResult: BitlyShortenResponse!
var testHTTPStatus: HTTPURLResponse!
// var testHTTPError: Error!
let expectation = XCTestExpectation(description: "URLShorten response")
testBitly.shorten(testURL, backhalf: nil, completion: { result in
switch result {
case .success((let resultData, let resultHTTPStatus)):
let decoder = JSONDecoder()
testHTTPStatus = resultHTTPStatus
testBitlyResult = try? decoder.decode(BitlyShortenResponse.self, from: resultData)
// dump("Result data: \(String(describing: testBitlyResult))")
// dump("Result HTTP status: \(resultHTTPStatus)")
case .failure(let resultError):
// testHTTPError = resultError
XCTFail("Failed to shorten URL: \(resultError)")
}
expectation.fulfill()
})
let fakeRequest = fakeShortenURLRequest(testURL)
spyURLSession.dataTaskArgsCompletionHandler.first?(
fakeShortenURLResponse(fakeRequest),
successHTTPURLResponse(to: fakeRequest),
nil
)
wait(for: [expectation], timeout: 0.1)
XCTAssertEqual(1, spyURLSession.dataTaskCallCount)
XCTAssertTrue(testBitlyResult.link.starts(with: "https://bit.ly"))
XCTAssertTrue(testBitlyResult.id.starts(with: "bit.ly"))
XCTAssertEqual(testURL.absoluteString, testBitlyResult.long_url)
XCTAssertEqual(200, testHTTPStatus.statusCode)
}
func test_shorten_successfulBitlyAsyncResponse() async throws {
let testURL = URL(string: "http://example.com/foo")!
var testBitlyResult: BitlyShortenResponse!
var testHTTPStatus: HTTPURLResponse!
// var testHTTPError: Error!
let fakeRequest = fakeShortenURLRequest(testURL)
spyURLSession.asyncDataResponse = fakeShortenURLResponse(fakeRequest)
spyURLSession.asyncURLResponse = successHTTPURLResponse(to: fakeRequest)
let result = try await testBitly.shorten(testURL, backhalf: nil)
switch result {
case .success((let resultData, let resultHTTPStatus)):
let decoder = JSONDecoder()
testHTTPStatus = resultHTTPStatus
testBitlyResult = try? decoder.decode(BitlyShortenResponse.self, from: resultData)
case .failure(let resultError):
XCTFail("Failed to shorten URL: \(resultError)")
}
XCTAssertEqual(1, spyURLSession.dataTaskCallCount)
XCTAssertTrue(testBitlyResult.link.starts(with: "https://bit.ly"))
XCTAssertTrue(testBitlyResult.id.starts(with: "bit.ly"))
XCTAssertEqual(testURL.absoluteString, testBitlyResult.long_url)
XCTAssertEqual(200, testHTTPStatus.statusCode)
}
// MARK: - Testing Helpers
private func generateISO8601Date(date: Date = Date()) -> String {
let formatter = ISO8601DateFormatter()
return formatter.string(from: date)
}
private func generateCurrentUNIXEpoch(date: Date = Date()) -> String {
return "\(Int(Date().timeIntervalSince1970))"
}
private func generateFakeBitlyID() -> String {
// use a common log date value
let dateStr = generateCurrentUNIXEpoch()
return "bit.ly/\(dateStr)"
}
func successHTTPURLResponse(to request: URLRequest) -> HTTPURLResponse {
return HTTPURLResponse(url: request.url ?? anyURL(),
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [:])!
}
func anyURL() -> URL {
return URL(string: "http://example.com")!
}
private func fakeShortenURLRequest(_ url: URL) -> URLRequest {
let bitlyURL = URL(string: "https://api-ssl.bitly.com/v4/shorten")!
var request = URLRequest(url: bitlyURL,
cachePolicy: .reloadIgnoringLocalCacheData)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer fakebitlyapitken", forHTTPHeaderField: "Authorization")
let encoder = JSONEncoder()
let reqData = BitlyShortenRequest(long_url: url.absoluteString,
domain: "bit.ly",
group_guid: "fakebitlyguid")
let encodedJSON = try? encoder.encode(reqData)
request.httpBody = encodedJSON
return request
}
private func fakeShortenURLResponse(_ request: URLRequest) -> Data {
let decoder = JSONDecoder()
let bitlyRequest = try? decoder.decode(BitlyShortenRequest.self,
from: request.httpBody!)
let encoder = JSONEncoder()
let fakeBitlyID = generateFakeBitlyID()
let reqData = BitlyShortenResponse(
created_at: generateISO8601Date(),
id: fakeBitlyID,
link: "https://" + fakeBitlyID,
long_url: bitlyRequest?.long_url ?? "",
archived: false,
custom_bitlinks: [],
tags: [],
deeplinks: []
)
let encodedJSON = try? encoder.encode(reqData)
return encodedJSON ?? Data("{}".utf8)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment