import HAKit import PromiseKit @testable import Shared import Version import XCTest class LocalPushManagerTests: XCTestCase { private var manager: LocalPushManager! private var api: FakeHomeAssistantAPI! private var apiConnection: HAMockConnection! private var attachmentManager: FakeNotificationAttachmentManager! private var added: [(UNNotificationRequest, Resolver)] = [] private var addedChanged: (() -> Void)? override func setUp() { super.setUp() attachmentManager = FakeNotificationAttachmentManager() Current.notificationAttachmentManager = attachmentManager let server: Server let fakeServers = FakeServerManager() Current.servers = fakeServers server = fakeServers.addFake() api = FakeHomeAssistantAPI(server: server) apiConnection = HAMockConnection() api.connection = apiConnection Current.cachedApis[server.identifier] = api added = [] } override func tearDown() { super.tearDown() weak var weakManager = manager manager = nil XCTAssertNil(weakManager) for sub in apiConnection.pendingSubscriptions { XCTAssertTrue(sub.cancellable.wasCancelled) } } private func setUpManager(webhookID: String, version: Version? = nil) { api.server.info.connection.webhookID = webhookID if let version { api.server.info.version = version } manager = LocalPushManager(server: api.server) manager.add = { [weak self] request in let (promise, resolver) = Promise.pending() self?.added.append((request, resolver)) DispatchQueue.main.async { self?.addedChanged?() } return promise } } private func fireConnectionChange() { api.server.info.connection.internalHardwareAddresses = [UUID().uuidString] let expectation = expectation(description: "loop") DispatchQueue.main.async { expectation.fulfill() } wait(for: [expectation], timeout: 10) } func testStateInitialSuccessful() throws { setUpManager(webhookID: "webhook1") XCTAssertEqual(manager.state, .establishing) let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.initiated(.success(.empty)) XCTAssertEqual(manager.state, .available(received: 0)) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", ])) XCTAssertEqual(manager.state, .available(received: 1)) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", ])) XCTAssertEqual(manager.state, .available(received: 2)) } func testStateInitialFailure() throws { setUpManager(webhookID: "webhook1") XCTAssertEqual(manager.state, .establishing) let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.initiated(.failure(.internal(debugDescription: "unit-test"))) XCTAssertEqual(manager.state, .unavailable) // pretend like a future connection made it work sub.initiated(.success(.empty)) XCTAssertEqual(manager.state, .available(received: 0)) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", ])) XCTAssertEqual(manager.state, .available(received: 1)) } func testSubscriptionAtStart() throws { setUpManager(webhookID: "webhook1", version: .init(major: 2021, minor: 9)) let sub1 = try XCTUnwrap(apiConnection.pendingSubscriptions.first) XCTAssertEqual(sub1.request.type, "mobile_app/push_notification_channel") XCTAssertEqual(sub1.request.data["webhook_id"] as? String, "webhook1") XCTAssertNil(sub1.request.data["support_confirm"]) sub1.initiated(.success(.empty)) apiConnection.pendingSubscriptions.removeAll() fireConnectionChange() XCTAssertTrue(apiConnection.pendingSubscriptions.isEmpty, "same id") api.server.info.version = .init(major: 2021, minor: 10) // change webhookID api.server.info.connection.webhookID = "webhook2" fireConnectionChange() XCTAssertTrue(sub1.cancellable.wasCancelled) let sub2 = try XCTUnwrap(apiConnection.pendingSubscriptions.first) XCTAssertEqual(sub2.request.type, "mobile_app/push_notification_channel") XCTAssertEqual(sub2.request.data["webhook_id"] as? String, "webhook2") XCTAssertEqual(sub2.request.data["support_confirm"] as? Bool, true) // fail the subscription sub2.initiated(.failure(.internal(debugDescription: "unit-test"))) fireConnectionChange() // now succeed it (e.g. reconnect happened) sub2.initiated(.success(.empty)) } func testInvalidate() throws { setUpManager(webhookID: "webhook1") let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) manager.invalidate() XCTAssertTrue(sub.cancellable.wasCancelled) } func testEventSuccessfullyAddedWithoutConfirmId() throws { setUpManager(webhookID: "webhook1") let expectation1 = expectation(description: "contentRequestsChanged") attachmentManager.contentRequestsChanged = { expectation1.fulfill() } let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", "data": [ "tag": "test_tag", ], ])) waitForExpectations(timeout: 10.0) let req = try XCTUnwrap(attachmentManager.contentRequests.first) XCTAssertEqual(req.0.body, "test_message") req.1(with(UNMutableNotificationContent()) { $0.body = "test_message_modified" }) let expectation2 = expectation(description: "addedChanged") addedChanged = { expectation2.fulfill() } waitForExpectations(timeout: 10.0) let final = try XCTUnwrap(added.first) XCTAssertEqual(final.0.content.body, "test_message_modified") XCTAssertEqual(final.0.identifier, "test_tag") final.1.fulfill(()) XCTAssertFalse( apiConnection.pendingRequests .contains(where: { $0.request.type == "mobile_app/push_notification_confirm" }) ) } func testEventSuccessfullyAddedWithConfirmIdSuccessfullyConfirm() throws { setUpManager(webhookID: "webhook1") let expectation1 = expectation(description: "contentRequestsChanged") attachmentManager.contentRequestsChanged = { expectation1.fulfill() } let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", "hass_confirm_id": "test_confirm_id", "data": [ "tag": "test_tag", ], ])) waitForExpectations(timeout: 10.0) let req = try XCTUnwrap(attachmentManager.contentRequests.first) XCTAssertEqual(req.0.body, "test_message") req.1(with(UNMutableNotificationContent()) { $0.body = "test_message_modified" }) let expectation2 = expectation(description: "addedChanged") addedChanged = { expectation2.fulfill() } waitForExpectations(timeout: 10.0) let final = try XCTUnwrap(added.first) XCTAssertEqual(final.0.content.body, "test_message_modified") XCTAssertEqual(final.0.identifier, "test_tag") final.1.fulfill(()) let expectation3 = expectation(description: "run loop") DispatchQueue.main.async(execute: expectation3.fulfill) waitForExpectations(timeout: 10.0) let pendingRequest = try XCTUnwrap( apiConnection.pendingRequests .first(where: { $0.request.type == "mobile_app/push_notification_confirm" }) ) XCTAssertEqual(pendingRequest.request.data["webhook_id"] as? String, "webhook1") XCTAssertEqual(pendingRequest.request.data["confirm_id"] as? String, "test_confirm_id") // just making sure this doesn't have a runtime problem pendingRequest.completion(.success(.empty)) } func testEventSuccessfullyAddedWithConfirmIdFailsToConfirm() throws { setUpManager(webhookID: "webhook1") let expectation1 = expectation(description: "contentRequestsChanged") attachmentManager.contentRequestsChanged = { expectation1.fulfill() } let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", "hass_confirm_id": "test_confirm_id", "data": [ "tag": "test_tag", ], ])) waitForExpectations(timeout: 10.0) let req = try XCTUnwrap(attachmentManager.contentRequests.first) XCTAssertEqual(req.0.body, "test_message") req.1(with(UNMutableNotificationContent()) { $0.body = "test_message_modified" }) let expectation2 = expectation(description: "addedChanged") addedChanged = { expectation2.fulfill() } waitForExpectations(timeout: 10.0) let final = try XCTUnwrap(added.first) XCTAssertEqual(final.0.content.body, "test_message_modified") XCTAssertEqual(final.0.identifier, "test_tag") final.1.fulfill(()) let expectation3 = expectation(description: "run loop") DispatchQueue.main.async(execute: expectation3.fulfill) waitForExpectations(timeout: 10.0) let pendingRequest = try XCTUnwrap( apiConnection.pendingRequests .first(where: { $0.request.type == "mobile_app/push_notification_confirm" }) ) XCTAssertEqual(pendingRequest.request.data["webhook_id"] as? String, "webhook1") XCTAssertEqual(pendingRequest.request.data["confirm_id"] as? String, "test_confirm_id") // just making sure this doesn't have a runtime problem pendingRequest.completion(.failure(.internal(debugDescription: "unit-test"))) } func testEventAddFailsWithoutConfirmId() throws { setUpManager(webhookID: "webhook1") let expectation1 = expectation(description: "contentRequestsChanged") attachmentManager.contentRequestsChanged = { expectation1.fulfill() } let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", "data": [ "tag": "test_tag", ], ])) waitForExpectations(timeout: 10.0) let req = try XCTUnwrap(attachmentManager.contentRequests.first) XCTAssertEqual(req.0.body, "test_message") req.1(with(UNMutableNotificationContent()) { $0.body = "test_message_modified" }) let expectation2 = expectation(description: "addedChanged") addedChanged = { expectation2.fulfill() } waitForExpectations(timeout: 10.0) let final = try XCTUnwrap(added.first) XCTAssertEqual(final.0.content.body, "test_message_modified") XCTAssertEqual(final.0.identifier, "test_tag") enum TestError: Error { case any } final.1.reject(TestError.any) } func testEventAddFailsWithConfirmId() throws { setUpManager(webhookID: "webhook1") let expectation1 = expectation(description: "contentRequestsChanged") attachmentManager.contentRequestsChanged = { expectation1.fulfill() } let sub = try XCTUnwrap(apiConnection.pendingSubscriptions.first) sub.handler(sub.cancellable, .dictionary([ "message": "test_message", "data": [ "tag": "test_tag", ], "hass_confirm_id": "test_confirm_id", ])) waitForExpectations(timeout: 10.0) let req = try XCTUnwrap(attachmentManager.contentRequests.first) XCTAssertEqual(req.0.body, "test_message") req.1(with(UNMutableNotificationContent()) { $0.body = "test_message_modified" }) let expectation2 = expectation(description: "addedChanged") addedChanged = { expectation2.fulfill() } waitForExpectations(timeout: 10.0) let final = try XCTUnwrap(added.first) XCTAssertEqual(final.0.content.body, "test_message_modified") XCTAssertEqual(final.0.identifier, "test_tag") enum TestError: Error { case any } final.1.reject(TestError.any) let expectation3 = expectation(description: "run loop") DispatchQueue.main.async(execute: expectation3.fulfill) waitForExpectations(timeout: 10.0) XCTAssertFalse( apiConnection.pendingRequests .contains(where: { $0.request.type == "mobile_app/push_notification_confirm" }) ) } } private class FakeNotificationAttachmentManager: NotificationAttachmentManager { var contentRequests: [(UNNotificationContent, (UNNotificationContent) -> Void)] = [] var contentRequestsChanged: (() -> Void)? func content( from originalContent: UNNotificationContent, api: HomeAssistantAPI ) -> Guarantee { let (guarantee, seal) = Guarantee.pending() contentRequests.append((originalContent, seal)) DispatchQueue.main.async { [contentRequestsChanged] in contentRequestsChanged?() } return guarantee } func downloadAttachment(from originalContent: UNNotificationContent, api: HomeAssistantAPI) -> Promise { fatalError() } } private class FakeHomeAssistantAPI: HomeAssistantAPI {}