diff --git a/Click2Minimize/AppDelegate.swift b/Click2Minimize/AppDelegate.swift index 55a4109..e16ba25 100644 --- a/Click2Minimize/AppDelegate.swift +++ b/Click2Minimize/AppDelegate.swift @@ -37,6 +37,7 @@ struct Click2MinimizeApp: App { } class AppDelegate: NSObject, NSApplicationDelegate { + var urlSession: URLSession = .shared var eventTap: CFMachPort? var mainWindow: NSWindow? var cancellables = Set() @@ -380,7 +381,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func checkForUpdates() { let url = URL(string: "https://api.github.com/repos/hatimhtm/Click2Minimize/releases/latest")! - let task = URLSession.shared.dataTask(with: url) { data, response, error in + let task = self.urlSession.dataTask(with: url) { data, response, error in guard let data = data, error == nil else { print("Error fetching updates: \(error?.localizedDescription ?? "Unknown error")") return @@ -426,9 +427,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - private func fetchLatestDMG(releaseInfo: Release) { + func fetchLatestDMG(releaseInfo: Release) { let url = URL(string: "https://api.github.com/repos/hatimhtm/Click2Minimize/releases/latest")! - let task = URLSession.shared.dataTask(with: url) { data, response, error in + let task = self.urlSession.dataTask(with: url) { data, response, error in guard let data = data, error == nil else { print("Error fetching release info: \(error?.localizedDescription ?? "Unknown error")") return @@ -444,6 +445,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { break } } + } else { + print("Error: Failed to parse JSON or missing 'assets' array.") } } task.resume() @@ -452,7 +455,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func downloadDMG(from urlString: String) { guard let url = URL(string: urlString) else { return } - let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in + let task = self.urlSession.downloadTask(with: url) { localURL, response, error in guard let localURL = localURL, error == nil else { print("Error downloading DMG: \(error?.localizedDescription ?? "Unknown error")") // Open the browser link for manual upgrade diff --git a/Click2MinimizeTests/AppDelegateTests.swift b/Click2MinimizeTests/AppDelegateTests.swift new file mode 100644 index 0000000..7a3bb61 --- /dev/null +++ b/Click2MinimizeTests/AppDelegateTests.swift @@ -0,0 +1,161 @@ +import XCTest +@testable import Click2Minimize + +class MockURLProtocol: URLProtocol { + static var mockData: Data? + static var mockResponse: URLResponse? + static var mockError: Error? + static var requestURLs: [URL] = [] + + static func reset() { + mockData = nil + mockResponse = nil + mockError = nil + requestURLs.removeAll() + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + if let url = request.url { + MockURLProtocol.requestURLs.append(url) + } + + if let error = MockURLProtocol.mockError { + self.client?.urlProtocol(self, didFailWithError: error) + } else { + if let response = MockURLProtocol.mockResponse { + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + } + if let data = MockURLProtocol.mockData { + self.client?.urlProtocol(self, didLoad: data) + } + } + self.client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() {} +} + +class AppDelegateTests: XCTestCase { + var appDelegate: AppDelegate! + + override func setUp() { + super.setUp() + appDelegate = AppDelegate() + + // Setup mock URLSession + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + let mockSession = URLSession(configuration: configuration) + + appDelegate.urlSession = mockSession + MockURLProtocol.reset() + } + + override func tearDown() { + appDelegate = nil + MockURLProtocol.reset() + super.tearDown() + } + + func testFetchLatestDMG_InvalidJSON() { + let invalidJSONString = "{ invalid_json: " + MockURLProtocol.mockData = invalidJSONString.data(using: .utf8) + + let releaseInfo = AppDelegate.Release(tag_name: "1.0.0") + + let expectation = XCTestExpectation(description: "Wait for fetch task to complete") + appDelegate.fetchLatestDMG(releaseInfo: releaseInfo) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + // Verify only 1 request was made (to the releases API), and no DMG download was triggered. + XCTAssertEqual(MockURLProtocol.requestURLs.count, 1, "Expected exactly 1 request for the release API.") + if let firstURL = MockURLProtocol.requestURLs.first { + XCTAssertTrue(firstURL.absoluteString.contains("releases/latest"), "Expected API URL.") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: 2.0) + } + + func testFetchLatestDMG_ValidJSON_MissingAssets() { + let validJSONString = "{\"not_assets\": []}" + MockURLProtocol.mockData = validJSONString.data(using: .utf8) + + let releaseInfo = AppDelegate.Release(tag_name: "1.0.0") + + let expectation = XCTestExpectation(description: "Wait for fetch task to complete") + appDelegate.fetchLatestDMG(releaseInfo: releaseInfo) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + // Verify only 1 request was made (to the releases API), and no DMG download was triggered. + XCTAssertEqual(MockURLProtocol.requestURLs.count, 1, "Expected exactly 1 request for the release API.") + expectation.fulfill() + } + wait(for: [expectation], timeout: 2.0) + } + + func testFetchLatestDMG_ValidJSON_NoDMG() { + let validJSONString = """ + { + "assets": [ + { + "browser_download_url": "https://example.com/app.zip", + "name": "app.zip" + } + ] + } + """ + MockURLProtocol.mockData = validJSONString.data(using: .utf8) + + let releaseInfo = AppDelegate.Release(tag_name: "1.0.0") + + let expectation = XCTestExpectation(description: "Wait for fetch task to complete") + appDelegate.fetchLatestDMG(releaseInfo: releaseInfo) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + // Verify only 1 request was made (to the releases API), and no DMG download was triggered. + XCTAssertEqual(MockURLProtocol.requestURLs.count, 1, "Expected exactly 1 request for the release API.") + expectation.fulfill() + } + wait(for: [expectation], timeout: 2.0) + } + + func testFetchLatestDMG_HappyPath() { + let validJSONString = """ + { + "assets": [ + { + "browser_download_url": "https://example.com/app.dmg", + "name": "app.dmg" + } + ] + } + """ + MockURLProtocol.mockData = validJSONString.data(using: .utf8) + + let releaseInfo = AppDelegate.Release(tag_name: "1.0.0") + + let expectation = XCTestExpectation(description: "Wait for fetch task to complete") + appDelegate.fetchLatestDMG(releaseInfo: releaseInfo) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + // Verify 2 requests were made (API fetch + DMG download) + XCTAssertEqual(MockURLProtocol.requestURLs.count, 2, "Expected 2 requests: one for API, one for DMG download.") + + if MockURLProtocol.requestURLs.count == 2 { + XCTAssertTrue(MockURLProtocol.requestURLs[0].absoluteString.contains("releases/latest"), "First request should be the API.") + XCTAssertTrue(MockURLProtocol.requestURLs[1].absoluteString.contains("app.dmg"), "Second request should be the DMG download.") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: 2.0) + } +}