Skip to content

Instantly share code, notes, and snippets.

@spicyjack
Created March 20, 2023 16:00
Show Gist options
  • Select an option

  • Save spicyjack/75fefd4c551607f62ae4ddd4dfdc9f6c to your computer and use it in GitHub Desktop.

Select an option

Save spicyjack/75fefd4c551607f62ae4ddd4dfdc9f6c to your computer and use it in GitHub Desktop.

Revisions

  1. spicyjack created this gist Mar 20, 2023.
    161 changes: 161 additions & 0 deletions BitlyURLShortenerTests.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,161 @@
    //
    // 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)
    }

    }