diff --git a/Networking/Networking/Network/MockNetwork.swift b/Networking/Networking/Network/MockNetwork.swift index 7bb663c85ad..613d769bae3 100644 --- a/Networking/Networking/Network/MockNetwork.swift +++ b/Networking/Networking/Network/MockNetwork.swift @@ -27,6 +27,10 @@ class MockNetwork: Network { /// var requestsForResponseData = [URLRequestConvertible]() + /// Number of notification objects in notifications-load-all.json file. + /// + static let notificationLoadAllJSONCount = 46 + /// Note: If the useResponseQueue param is `true`, any responses added via `simulateResponse` will stored in a FIFO queue /// and used once for a matching request (then removed from the queue). Subsequent requests will use the next response in the queue, and so on. /// diff --git a/Networking/NetworkingTests/Mapper/NoteListMapperTests.swift b/Networking/NetworkingTests/Mapper/NoteListMapperTests.swift index 2dce7324e95..7c61b8783ed 100644 --- a/Networking/NetworkingTests/Mapper/NoteListMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/NoteListMapperTests.swift @@ -24,7 +24,7 @@ final class NoteListMapperTests: XCTestCase { /// Verifies that all of the Sample Notifications are properly parsed. /// func test_sample_notifications_are_properly_decoded() { - XCTAssertEqual(sampleNotes.count, 44) + XCTAssertEqual(sampleNotes.count, MockNetwork.notificationLoadAllJSONCount) } /// Verifies that the Broken Notification documents are properly parsed. diff --git a/Networking/NetworkingTests/Remote/NotificationsRemoteTests.swift b/Networking/NetworkingTests/Remote/NotificationsRemoteTests.swift index 3417467de43..3697b4300d7 100644 --- a/Networking/NetworkingTests/Remote/NotificationsRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/NotificationsRemoteTests.swift @@ -40,7 +40,7 @@ final class NotificationsRemoteTests: XCTestCase { XCTAssertTrue(result.isSuccess) let notes = try result.get() - XCTAssertEqual(notes.count, 44) + XCTAssertEqual(notes.count, MockNetwork.notificationLoadAllJSONCount) } /// Verifies that `loadHashes` properly returns all of the retrieved hashes. diff --git a/Networking/NetworkingTests/Responses/notifications-load-all.json b/Networking/NetworkingTests/Responses/notifications-load-all.json index ae239b1bba5..62007213530 100644 --- a/Networking/NetworkingTests/Responses/notifications-load-all.json +++ b/Networking/NetworkingTests/Responses/notifications-load-all.json @@ -7671,6 +7671,102 @@ } ] }, + { + "id": 100041, + "note_hash": 987654, + "type": "store_order", + "read": false, + "noticon": "\uf447", + "timestamp": "2018-10-22T15:29:36+00:00", + "icon": "https:\/\/s.wp.com\/wp-content\/mu-plugins\/notes\/images\/update-payment-2x.png", + "url": "https:\/\/aaaaaaaaa.mystagingwebsite.com\/wp-admin\/post.php?post=1476&action=edit", + "subject": [{ + "text": "\ud83c\udf89 You have a new order!" + }, { + "text": "Someone placed a $20.00 order from Bananza World", + "ranges": [{ + "type": "site", + "indices": [35, 48], + "url": "https:\/\/somewhere.tld", + "id": 123456 + }] + }], + "body": [{ + "text": "", + "media": [{ + "type": "image", + "indices": [0, 0], + "height": "48", + "width": "48", + "url": "https:\/\/s.wp.com\/wp-content\/mu-plugins\/notes\/images\/store-cart-icon.png" + }] + }, { + "text": "Order Number: 1476\nDate: October 22, 2018\nTotal: $20.00\nPayment Method: Credit Card (Stripe)\nShipping Method: Free shipping" + }, { + "text": "Products:\n\nAlmonds \u00d7 1\n" + }, { + "text": "\u2139\ufe0f View Order", + "ranges": [{ + "url": "https:\/\/somewhere.tld\/wp-admin\/post.php?post=1476&action=edit", + "indices": [0, 13], + "type": "link" + }] + }], + "meta": { + "ids": { + "order": 123456 + } + }, + "title": "New Order" + }, + { + "id": 100042, + "note_hash": 987654, + "type": "store_order", + "read": true, + "noticon": "\uf447", + "timestamp": "2018-10-22T15:29:36+00:00", + "icon": "https:\/\/s.wp.com\/wp-content\/mu-plugins\/notes\/images\/update-payment-2x.png", + "url": "https:\/\/aaaaaaaaa.mystagingwebsite.com\/wp-admin\/post.php?post=1476&action=edit", + "subject": [{ + "text": "\ud83c\udf89 You have a new order!" + }, { + "text": "Someone placed a $20.00 order from Bananza World", + "ranges": [{ + "type": "site", + "indices": [35, 48], + "url": "https:\/\/somewhere.tld", + "id": 123456 + }] + }], + "body": [{ + "text": "", + "media": [{ + "type": "image", + "indices": [0, 0], + "height": "48", + "width": "48", + "url": "https:\/\/s.wp.com\/wp-content\/mu-plugins\/notes\/images\/store-cart-icon.png" + }] + }, { + "text": "Order Number: 1476\nDate: October 22, 2018\nTotal: $20.00\nPayment Method: Credit Card (Stripe)\nShipping Method: Free shipping" + }, { + "text": "Products:\n\nAlmonds \u00d7 1\n" + }, { + "text": "\u2139\ufe0f View Order", + "ranges": [{ + "url": "https:\/\/somewhere.tld\/wp-admin\/post.php?post=1476&action=edit", + "indices": [0, 13], + "type": "link" + }] + }], + "meta": { + "ids": { + "order": 123456 + } + }, + "title": "New Order" + }, { "id": 8401476681, "type": "blaze_approved_note", diff --git a/WooCommerce/Classes/Model/MarkOrderAsReadUseCase.swift b/WooCommerce/Classes/Model/MarkOrderAsReadUseCase.swift index 813609a9857..c7852cec166 100644 --- a/WooCommerce/Classes/Model/MarkOrderAsReadUseCase.swift +++ b/WooCommerce/Classes/Model/MarkOrderAsReadUseCase.swift @@ -40,7 +40,8 @@ struct MarkOrderAsReadUseCase { switch syncronizedNoteResult { case .success(let syncronizedNote): guard let syncronizedNoteOrderID = syncronizedNote.meta.identifier(forKey: .order), - syncronizedNoteOrderID == orderID else { + syncronizedNoteOrderID == orderID, + syncronizedNote.read == false else { return .failure(MarkOrderAsReadUseCase.Error.noNeedToMarkAsRead) } @@ -88,11 +89,14 @@ struct MarkOrderAsReadUseCase { switch loadedNotes { case .success(let notes): - guard let note = notes.first else { + guard let note = notes.first(where: { goalNote in + return goalNote.noteID == noteID + }) else { return .failure(MarkOrderAsReadUseCase.Error.unavailableNote) } - guard let syncronizedNoteOrderID = note.meta.identifier(forKey: .order), - syncronizedNoteOrderID == orderID else { + guard let loadedNoteOrderID = note.meta.identifier(forKey: .order), + loadedNoteOrderID == orderID, + note.read == false else { return .failure(MarkOrderAsReadUseCase.Error.noNeedToMarkAsRead) } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 5406f6ad03f..85b1afc5fe9 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -2446,6 +2446,7 @@ DA1D68C22C36F0980097859A /* PointOfSaleAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1D68C12C36F0980097859A /* PointOfSaleAssets.swift */; }; DA25ADDD2C86145E00AE81FE /* MarkOrderAsReadUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA25ADDC2C86145E00AE81FE /* MarkOrderAsReadUseCase.swift */; }; DA25ADDF2C87403900AE81FE /* PushNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA25ADDE2C87403900AE81FE /* PushNotificationTests.swift */; }; + DA3F99BA2C92F6D30034BDA5 /* MarkOrderAsReadUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3F99B92C92F6D30034BDA5 /* MarkOrderAsReadUseCaseTests.swift */; }; DA41043A2C247B6900E8456A /* POSOrderPreviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4104392C247B6900E8456A /* POSOrderPreviewService.swift */; }; DA706BF92C8063B600E08A5B /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575472802452185300A94C3C /* PushNotification.swift */; }; DA706BFA2C80642800E08A5B /* Dictionary+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57C5C9521B80E5400FF82B2 /* Dictionary+Woo.swift */; }; @@ -5511,6 +5512,7 @@ DA1D68C12C36F0980097859A /* PointOfSaleAssets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointOfSaleAssets.swift; sourceTree = ""; }; DA25ADDC2C86145E00AE81FE /* MarkOrderAsReadUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkOrderAsReadUseCase.swift; sourceTree = ""; }; DA25ADDE2C87403900AE81FE /* PushNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationTests.swift; sourceTree = ""; }; + DA3F99B92C92F6D30034BDA5 /* MarkOrderAsReadUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkOrderAsReadUseCaseTests.swift; sourceTree = ""; }; DA4104392C247B6900E8456A /* POSOrderPreviewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderPreviewService.swift; sourceTree = ""; }; DABF35262C11B426006AF826 /* PointOfSaleDashboardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleDashboardViewModelTests.swift; sourceTree = ""; }; DAD988C52C4A9CF9009DE9E3 /* CartItem+Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CartItem+Order.swift"; sourceTree = ""; }; @@ -10561,6 +10563,7 @@ 2631D4F929ED108400F13F20 /* WPComPlanNameSanitizer.swift */, B98FF43F2AAA096200326D16 /* AddressWooTests.swift */, 023BD5852BFDCECF00A10D7B /* BetaFeaturesConfigurationViewModelTests.swift */, + DA3F99B92C92F6D30034BDA5 /* MarkOrderAsReadUseCaseTests.swift */, ); path = Model; sourceTree = ""; @@ -16641,6 +16644,7 @@ EEB221A729B9B5B300662A12 /* CouponLineDetailsViewModelTests.swift in Sources */, 026D684B2A0E0A9600D8C22C /* LocalNotificationSchedulerTests.swift in Sources */, 02038C612AF222D600CD36D9 /* ConfigurableVariableBundleAttributePickerViewModelTests.swift in Sources */, + DA3F99BA2C92F6D30034BDA5 /* MarkOrderAsReadUseCaseTests.swift in Sources */, 26C0D1E32B460E5700F6EDA5 /* OrderNotificationViewModel.swift in Sources */, 86E40AED2B597DEC00990365 /* BlazeCampaignCreationCoordinatorTests.swift in Sources */, 20A3AFE12B0F750B0033AF2D /* MockInPersonPaymentsCashOnDeliveryToggleRowViewModel.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Model/MarkOrderAsReadUseCaseTests.swift b/WooCommerce/WooCommerceTests/Model/MarkOrderAsReadUseCaseTests.swift new file mode 100644 index 00000000000..eac7983300a --- /dev/null +++ b/WooCommerce/WooCommerceTests/Model/MarkOrderAsReadUseCaseTests.swift @@ -0,0 +1,163 @@ +import XCTest +import WooFoundation +@testable import Yosemite +@testable import Networking +@testable import Storage + +final class MarkOrderAsReadUseCaseTests: XCTestCase { + private var dispatcher: Dispatcher! + private var network: MockNetwork! + private var storageManager: MockStorageManager! + private var storesManager: MockStoresManager! + private var viewStorage: StorageType { + return storageManager.viewStorage + } + private lazy var sampleNotes: [Yosemite.Note] = { + return try! mapNotes(from: "notifications-load-all") + }() + + private func sampleNote(read: Bool) -> Yosemite.Note? { + return sampleNotes.first { note in + return note.read == read && note.meta.identifier(forKey: .order) != nil + } + } + + override func setUp() { + super.setUp() + dispatcher = Dispatcher() + storageManager = MockStorageManager() + storesManager = MockStoresManager(sessionManager: .makeForTesting()) + network = MockNetwork() + + NotificationStore.resetSharedDerivedStorage() + } + + override func tearDown() { + NotificationStore.resetSharedDerivedStorage() + super.tearDown() + } + + private func setupStoreManagerReceivingNotificationActions(for note: Yosemite.Note, noteStore: NotificationStore) { + storesManager.whenReceivingAction(ofType: NotificationAction.self) { action in + switch action { + case let .synchronizeNotifications(onCompletion): + onCompletion(nil) + case let .synchronizeNotification(_, onCompletion): + onCompletion(note, nil) + case let .updateReadStatus(noteID, read, onCompletion): + noteStore.updateLocalNoteReadStatus(for: [noteID], read: read) { + onCompletion(nil) + } + default: + XCTFail("Unexpected action: \(action)") + } + } + } + + @MainActor + func test_markOrderNoteAsReadIfNeeded_with_stores_unreadNote() async throws { + let unreadNote = try XCTUnwrap(sampleNote(read: false)) + let orderID = try XCTUnwrap(unreadNote.meta.identifier(forKey: .order)) + + let noteStore = NotificationStore(dispatcher: dispatcher, storageManager: storageManager, network: network) + + setupStoreManagerReceivingNotificationActions(for: unreadNote, noteStore: noteStore) + + await withCheckedContinuation { continuation in + noteStore.updateLocalNotes(with: [unreadNote]) { + XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), 1) + continuation.resume() + } + } + + let result = await MarkOrderAsReadUseCase.markOrderNoteAsReadIfNeeded(stores: storesManager, noteID: unreadNote.noteID, orderID: orderID) + switch result { + case .success(let markedNote): + XCTAssertEqual(unreadNote.noteID, markedNote.noteID) + let storageNote = viewStorage.loadNotification(noteID: markedNote.noteID) + XCTAssertEqual(storageNote?.read, true) + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + + @MainActor + func test_markOrderNoteAsReadIfNeeded_with_stores_alreadyReadNote() async throws { + let readNote = try XCTUnwrap(sampleNote(read: true)) + let orderID = try XCTUnwrap(readNote.meta.identifier(forKey: .order)) + + let noteStore = NotificationStore(dispatcher: dispatcher, storageManager: storageManager, network: network) + + setupStoreManagerReceivingNotificationActions(for: readNote, noteStore: noteStore) + + await withCheckedContinuation { continuation in + noteStore.updateLocalNotes(with: [readNote]) { + XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), 1) + continuation.resume() + } + } + + let result = await MarkOrderAsReadUseCase.markOrderNoteAsReadIfNeeded(stores: storesManager, noteID: readNote.noteID, orderID: orderID) + switch result { + case .success: + XCTFail("Note was already read, it should not be marked as read again.") + case .failure(let error): + if case MarkOrderAsReadUseCase.Error.noNeedToMarkAsRead = error {} else { + XCTFail("Got wrong error \(error.localizedDescription)") + } + } + } + + @MainActor + func test_markOrderNoteAsReadIfNeeded_with_network_unreadNote() async throws { + let unreadNote = try XCTUnwrap(sampleNote(read: false)) + let orderID = try XCTUnwrap(unreadNote.meta.identifier(forKey: .order)) + + network.simulateResponse(requestUrlSuffix: "notifications", filename: "notifications-load-all") + network.simulateResponse(requestUrlSuffix: "notifications/read", filename: "generic_success") + + let result = await MarkOrderAsReadUseCase.markOrderNoteAsReadIfNeeded(network: network, + noteID: unreadNote.noteID, + orderID: orderID) + + switch result { + case .success(let markedNoteID): + XCTAssertEqual(unreadNote.noteID, markedNoteID) + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + + @MainActor + func test_markOrderNoteAsReadIfNeeded_with_network_alreadyReadNote() async throws { + let readNote = try XCTUnwrap(sampleNote(read: true)) + let orderID = try XCTUnwrap(readNote.meta.identifier(forKey: .order)) + + network.simulateResponse(requestUrlSuffix: "notifications", filename: "notifications-load-all") + + let result = await MarkOrderAsReadUseCase.markOrderNoteAsReadIfNeeded(network: network, + noteID: readNote.noteID, + orderID: orderID) + + switch result { + case .success: + XCTFail("Note was already read, it should not be marked as read again.") + case .failure(let error): + if case MarkOrderAsReadUseCase.Error.noNeedToMarkAsRead = error {} else { + XCTFail("Got wrong error \(error.localizedDescription)") + } + } + } +} + +/// Private Methods. +/// +private extension MarkOrderAsReadUseCaseTests { + + /// Returns the NoteListMapper output upon receiving `filename` (Data Encoded) + /// + func mapNotes(from filename: String) throws -> [Yosemite.Note] { + let response = Loader.contentsOf(filename)! + return try NoteListMapper().map(response: response) + } +} diff --git a/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift b/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift index 566d911e924..97cef08d3c7 100644 --- a/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift @@ -49,7 +49,7 @@ class NotificationStoreTests: XCTestCase { XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Note.self), 0) let action = NotificationAction.synchronizeNotifications() { (error) in XCTAssertNil(error) - XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), 44) + XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), MockNetwork.notificationLoadAllJSONCount) if let note = self.viewStorage.loadNotification(noteID: 100036, noteHash: 987654)?.toReadOnly() { // Plain Fields @@ -111,7 +111,7 @@ class NotificationStoreTests: XCTestCase { /// Initial Sync /// let initialSyncAction = NotificationAction.synchronizeNotifications() { (error) in - XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), 44) + XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), MockNetwork.notificationLoadAllJSONCount) notificationStore.onAction(nestedSyncAction) }