diff --git a/BBus/BBus.xcodeproj/project.pbxproj b/BBus/BBus.xcodeproj/project.pbxproj index 675aa2d6..06e28c4b 100644 --- a/BBus/BBus.xcodeproj/project.pbxproj +++ b/BBus/BBus.xcodeproj/project.pbxproj @@ -61,6 +61,27 @@ 4A1A22DB27326FD100476861 /* HomeNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1A22DA27326FD100476861 /* HomeNavigationView.swift */; }; 4A1A22DD2732801700476861 /* StationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1A22DC2732801700476861 /* StationCoordinator.swift */; }; 4A1A22E12732CE7900476861 /* SearchResultCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1A22E02732CE7900476861 /* SearchResultCollectionViewCell.swift */; }; + 4A2634AC27566F8600267B47 /* StationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5216272FCE8E00EC0531 /* StationViewModel.swift */; }; + 4A2634AF275670A500267B47 /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 4A2634B0275670BE00267B47 /* StationByUidItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B8273BE2640061ACDA /* StationByUidItemDTO.swift */; }; + 4A2634B1275670C500267B47 /* FavoriteItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEC7274159D10027222D /* FavoriteItemDTO.swift */; }; + 4A2634B2275670CB00267B47 /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; }; + 4A2634B3275670D100267B47 /* StationDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AE273BAA130061ACDA /* StationDTO.swift */; }; + 4A2634B62756754E00267B47 /* BusSectionKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACC26B3274F669800173A32 /* BusSectionKeys.swift */; }; + 4A2634B72756755700267B47 /* BBusRouteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047C30FD273A05A60075EA14 /* BBusRouteType.swift */; }; + 4A2634B82756756200267B47 /* BusArriveInfos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACC26B7274F6BD300173A32 /* BusArriveInfos.swift */; }; + 4A2634B92756757400267B47 /* NotificationNameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEE82743DAB20027222D /* NotificationNameExtension.swift */; }; + 4A2634BA2756757F00267B47 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 4A2634BB2756758C00267B47 /* BusCongestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5212272FCE8500EC0531 /* BusCongestion.swift */; }; + 4A2634BC2756759700267B47 /* AlarmSettingBusArriveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA521E272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift */; }; + 4A2634BD275675A800267B47 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 4A2634BE275675C400267B47 /* BusRemainTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A094CDE27435C5900428F55 /* BusRemainTime.swift */; }; + 4A2634BF275675CD00267B47 /* BusPosByVehicleIdDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29BA274B6C8300554A4E /* BusPosByVehicleIdDTO.swift */; }; + 4A2634C12756862F00267B47 /* StationAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468430275676ED007E440A /* StationAPIUsable.swift */; }; + 4A2634C22756863600267B47 /* StationCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468432275676F8007E440A /* StationCalculatable.swift */; }; + 4A2634C32756864A00267B47 /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 4A2634C427568CB900267B47 /* StationCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DC47FA27552FE5003380D9 /* StationCalculateUseCase.swift */; }; + 4A2634C5275693BC00267B47 /* DummyJsonStringStationByUidItemDTO.json in Resources */ = {isa = PBXBuildFile; fileRef = 4A2634C027567E5400267B47 /* DummyJsonStringStationByUidItemDTO.json */; }; 4A38E20727463FF7003A9D10 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; 4A38E20927464015003A9D10 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; 4A3B6C502755DF2400BBC498 /* AlarmCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3B6C4F2755DF2400BBC498 /* AlarmCenter.swift */; }; @@ -369,6 +390,7 @@ 4A1A22DA27326FD100476861 /* HomeNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeNavigationView.swift; sourceTree = ""; }; 4A1A22DC2732801700476861 /* StationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationCoordinator.swift; sourceTree = ""; }; 4A1A22E02732CE7900476861 /* SearchResultCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCollectionViewCell.swift; sourceTree = ""; }; + 4A2634C027567E5400267B47 /* DummyJsonStringStationByUidItemDTO.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DummyJsonStringStationByUidItemDTO.json; sourceTree = ""; }; 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BBusAPIError.swift; sourceTree = ""; }; 4A38E20827464015003A9D10 /* PublisherExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherExtension.swift; sourceTree = ""; }; 4A3B6C4F2755DF2400BBC498 /* AlarmCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCenter.swift; sourceTree = ""; }; @@ -1171,6 +1193,7 @@ isa = PBXGroup; children = ( 4AF1E0B52756269600DE51C8 /* StationViewModelTests.swift */, + 4A2634C027567E5400267B47 /* DummyJsonStringStationByUidItemDTO.json */, ); path = StationViewModelTests; sourceTree = ""; @@ -1684,6 +1707,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4A2634C5275693BC00267B47 /* DummyJsonStringStationByUidItemDTO.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1790,7 +1814,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4A2634BB2756758C00267B47 /* BusCongestion.swift in Sources */, + 4A2634B0275670BE00267B47 /* StationByUidItemDTO.swift in Sources */, + 4A2634AF275670A500267B47 /* BaseUseCase.swift in Sources */, + 4A2634C22756863600267B47 /* StationCalculatable.swift in Sources */, + 4A2634BC2756759700267B47 /* AlarmSettingBusArriveInfo.swift in Sources */, + 4A2634B1275670C500267B47 /* FavoriteItemDTO.swift in Sources */, + 4A2634B3275670D100267B47 /* StationDTO.swift in Sources */, + 4A2634BA2756757F00267B47 /* PublisherExtension.swift in Sources */, + 4A2634BE275675C400267B47 /* BusRemainTime.swift in Sources */, + 4A2634BF275675CD00267B47 /* BusPosByVehicleIdDTO.swift in Sources */, + 4A2634C12756862F00267B47 /* StationAPIUsable.swift in Sources */, + 4A2634B62756754E00267B47 /* BusSectionKeys.swift in Sources */, + 4A2634BD275675A800267B47 /* BBusAPIError.swift in Sources */, + 4A2634C32756864A00267B47 /* JsonDTO.swift in Sources */, 4AF1E0B62756269600DE51C8 /* StationViewModelTests.swift in Sources */, + 4A2634B72756755700267B47 /* BBusRouteType.swift in Sources */, + 4A2634B92756757400267B47 /* NotificationNameExtension.swift in Sources */, + 4A2634B82756756200267B47 /* BusArriveInfos.swift in Sources */, + 4A2634C427568CB900267B47 /* StationCalculateUseCase.swift in Sources */, + 4A2634B2275670CB00267B47 /* BusRouteDTO.swift in Sources */, + 4A2634AC27566F8600267B47 /* StationViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2443,7 +2487,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B3PWYBKFUK; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2467,7 +2511,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B3PWYBKFUK; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift b/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift index b8d89958..6aa78a47 100644 --- a/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift +++ b/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift @@ -7,7 +7,6 @@ import Foundation import Combine -import UIKit final class StationViewModel { @@ -30,7 +29,10 @@ final class StationViewModel { self.calculateUseCase = calculateUseCase self.arsId = arsId self.busRouteList = [] + self.stationInfo = nil self.busKeys = BusSectionKeys() + self.favoriteItems = nil + self.nextStation = nil self.activeBuses = [:] self.inActiveBuses = [:] self.cancellables = [] @@ -55,14 +57,12 @@ final class StationViewModel { self?.error = error }) .combineLatest(self.$busRouteList.filter { !$0.isEmpty }) { (busRouteList, entireBusRouteList) in - busRouteList.filter { busRoute in + return busRouteList.filter { busRoute in entireBusRouteList.contains{ $0.routeID == busRoute.busRouteId } } } - .sink(receiveCompletion: { error in - print(error) - }, receiveValue: { [weak self] arriveInfo in - self?.nextStation = arriveInfo[0].nextStation + .sink(receiveValue: { [weak self] arriveInfo in + self?.nextStation = arriveInfo.first?.nextStation self?.classifyByRouteType(with: arriveInfo) }) .store(in: &self.cancellables) @@ -75,29 +75,27 @@ final class StationViewModel { } private func bind() { + self.bindLoader() self.bindStationInfo(with: self.arsId) self.bindBusRouteList() - self.bindLoader() self.bindFavoriteItems() } private func bindStationInfo(with arsId: String) { self.apiUseCase.loadStationList() - .tryMap({ [weak self] stations in + .map({ [weak self] stations in return self?.calculateUseCase.findStation(in: stations, with: arsId) }) + .tryMap({ stationInfo in + guard let stationInfo = stationInfo else { + throw BBusAPIError.invalidStationError + } + return stationInfo + }) .catchError({ [weak self] error in self?.error = error }) - .sink { [weak self] stationInfo in - if let stationInfo = stationInfo { - self?.stationInfo = stationInfo - } - else { - self?.error = BBusAPIError.invalidStationError - } - } - .store(in: &self.cancellables) + .assign(to: &self.$stationInfo) } private func bindBusRouteList() { @@ -114,10 +112,10 @@ final class StationViewModel { .catchError({ [weak self] error in self?.error = error }) - .sink(receiveValue: { [weak self] items in - self?.favoriteItems = items.filter() { $0.arsId == self?.arsId } + .map({ [weak self] items -> [FavoriteItemDTO] in + return items.filter({ $0.arsId == self?.arsId }) }) - .store(in: &self.cancellables) + .assign(to: &self.$favoriteItems) } private func classifyByRouteType(with buses: [StationByUidItemDTO]) { @@ -200,7 +198,8 @@ final class StationViewModel { } private func bindLoader() { - self.$busKeys.zip(self.$favoriteItems, self.$stationInfo) + self.$busKeys + .zip(self.$favoriteItems, self.$stationInfo) .output(at: 1) .sink(receiveValue: { [weak self] _ in self?.stopLoader = true diff --git a/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift b/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift index fa2dfe62..d3f19b77 100644 --- a/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift +++ b/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift @@ -21,7 +21,6 @@ extension ServiceFetchable { guard let key = try? self.tokenManager.randomAccessKey() else { return BBusAPIError.noMoreAccessKeyError.publisher } guard let request = self.requestFactory.request(url: url, accessKey: key.key, params: param) else { return NetworkError.urlError.publisher } - return networkService.get(request: request, params: param) .mapJsonBBusAPIError { self.tokenManager.removeAccessKey(at: key.index) diff --git a/BBus/StationViewModelTests/DummyJsonStringStationByUidItemDTO.json b/BBus/StationViewModelTests/DummyJsonStringStationByUidItemDTO.json new file mode 100644 index 00000000..67ada527 --- /dev/null +++ b/BBus/StationViewModelTests/DummyJsonStringStationByUidItemDTO.json @@ -0,0 +1,231 @@ +{ + "comMsgHeader": { + "responseMsgID": null, + "responseTime": null, + "requestMsgID": null, + "returnCode": null, + "errMsg": null, + "successYN": null + }, + "msgHeader": { + "headerMsg": "정상적으로 처리되었습니다.", + "headerCd": "0", + "itemCount": 0 + }, + "msgBody": { + "itemList": [ + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "100100496", + "rtNm": "333", + "sectNm": "위례35단지아파트~위례중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0400 ", + "lastTm": "2300 ", + "term": "12", + "routeType": "3", + "nextBus": " ", + "staOrd": "7", + "vehId1": "0", + "plainNo1": null, + "sectOrd1": "0", + "stationNm1": null, + "traTime1": "0", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": "2021-04-15 12:08:39.0", + "isLast1": "0", + "busType1": "1", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "올림픽공원", + "arrmsg1": "2분20초후[1번째 전]", + "arrmsg2": "7분20초후[3번째 전]", + "arrmsgSec1": "첫 번째 버스 운행종료", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "송파와이즈더샵.엠코타운센트로엘", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + }, + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "107000002", + "rtNm": "343", + "sectNm": "위례35단지아파트~위례중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0430 ", + "lastTm": "2300 ", + "term": "15", + "routeType": "4", + "nextBus": " ", + "staOrd": "7", + "vehId1": "0", + "plainNo1": null, + "sectOrd1": "0", + "stationNm1": null, + "traTime1": "0", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": "2021-04-15 12:09:10.0", + "isLast1": "0", + "busType1": "0", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "수서역", + "arrmsg1": "운행종료", + "arrmsg2": "운행종료", + "arrmsgSec1": "첫 번째 버스 운행종료", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "서울위례별초교", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + }, + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "100100459", + "rtNm": "440", + "sectNm": "위례35단지아파트~위례중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0355 ", + "lastTm": "2255 ", + "term": "12", + "routeType": "2", + "nextBus": " ", + "staOrd": "7", + "vehId1": "0", + "plainNo1": null, + "sectOrd1": "0", + "stationNm1": null, + "traTime1": "0", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": "2021-04-15 12:08:32.0", + "isLast1": "0", + "busType1": "1", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "압구정동한양파출소앞", + "arrmsg1": "운행종료", + "arrmsg2": "운행종료", + "arrmsgSec1": "첫 번째 버스 운행종료", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "송파와이즈더샵.엠코타운센트로엘", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + }, + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "227000035", + "rtNm": "9202하남", + "sectNm": "엠코타운플로리체~위례신안인스빌.중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0430 ", + "lastTm": "2220 ", + "term": "42", + "routeType": "8", + "nextBus": " ", + "staOrd": "90", + "vehId1": "227000164", + "plainNo1": null, + "sectOrd1": "90", + "stationNm1": "위례신안인스빌.중앙푸르지오", + "traTime1": "5", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": null, + "isLast1": "0", + "busType1": "0", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "상산곡동.공영차고지", + "arrmsg1": "곧 도착", + "arrmsg2": "운행종료", + "arrmsgSec1": "곧 도착", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "송파와이즈더샵.엠코타운센트로엘", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + } + ] + } +} diff --git a/BBus/StationViewModelTests/StationViewModelTests.swift b/BBus/StationViewModelTests/StationViewModelTests.swift index d65f3b15..81b2a611 100644 --- a/BBus/StationViewModelTests/StationViewModelTests.swift +++ b/BBus/StationViewModelTests/StationViewModelTests.swift @@ -6,8 +6,126 @@ // import XCTest +import Combine class StationViewModelTests: XCTestCase { + + enum TestError: Error { + case unknownError, urlError, dataError, noneError + + func publisher() -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + publisher.send(completion: .failure(self)) + return publisher.compactMap({$0}) + .eraseToAnyPublisher() + } + } + + enum MOCKType { + case success, failure + } + + struct MOCKStationAPIUseCase: StationAPIUsable { + + private let type: MOCKType + + init(_ type: MOCKType) { + self.type = type + } + + func loadStationList() -> AnyPublisher<[StationDTO], Error> { + let stationDTOs: [StationDTO] + switch self.type { + case .success : + stationDTOs = [StationDTO(stationID: 1, arsID: "1", stationName: "station1"), + StationDTO(stationID: 2, arsID: "2", stationName: "station2"), + StationDTO(stationID: 3, arsID: "3", stationName: "station3"), + StationDTO(stationID: 4, arsID: "4", stationName: "station4")] + case .failure : + stationDTOs = [StationDTO(stationID: 1, arsID: "2", stationName: "station1"), + StationDTO(stationID: 2, arsID: "3", stationName: "station2"), + StationDTO(stationID: 3, arsID: "4", stationName: "station3"), + StationDTO(stationID: 4, arsID: "5", stationName: "station4")] + } + return stationDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } + + func refreshInfo(about arsId: String) -> AnyPublisher<[StationByUidItemDTO], Error> { + guard let url = Bundle.init(identifier: "com.boostcamp.ios-009.StationViewModelTests")?.url(forResource: "DummyJsonStringStationByUidItemDTO", withExtension: "json") else { + return TestError.urlError.publisher() + } + if let data = try? Data(contentsOf: url), + let stationByUidDTOs = (try? JSONDecoder().decode(StationByUidItemResult.self, from: data))?.msgBody.itemList { + return stationByUidDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } else { + return TestError.dataError.publisher() + } + } + + func add(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + let data = try? PropertyListEncoder().encode(favoriteItem) + return data.publisher + .setFailureType(to: Error.self) + .compactMap({$0}) + .eraseToAnyPublisher() + } + + func remove(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + self.add(favoriteItem: favoriteItem) + } + + func getFavoriteItems() -> AnyPublisher<[FavoriteItemDTO], Error> { + let favoriteItemsDTOs = [FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1"), + FavoriteItemDTO(stId: "1", busRouteId: "3", ord: "2", arsId: "4"), + FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1"), + FavoriteItemDTO(stId: "3", busRouteId: "2", ord: "2", arsId: "2")] + return favoriteItemsDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } + + func loadRoute() -> AnyPublisher<[BusRouteDTO], Error> { + let busRouteDTOs: [BusRouteDTO] + switch self.type { + case .success : + busRouteDTOs = [BusRouteDTO(routeID: 100100496, + busRouteName: "bus1", + routeType: RouteType.broadArea, + startStation: "station1", + endStation: "station5"), + BusRouteDTO(routeID: 100100459, + busRouteName: "bus2", + routeType: RouteType.broadArea, + startStation: "station2", + endStation: "station100"), + BusRouteDTO(routeID: 107000002, + busRouteName: "bus3", + routeType: RouteType.circulation, + startStation: "station1", + endStation: "station5")] + case .failure : + busRouteDTOs = [BusRouteDTO(routeID: 1, + busRouteName: "bus1", + routeType: RouteType.broadArea, + startStation: "station1", + endStation: "station5")] + } + return busRouteDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } + } + + private let timeout: TimeInterval = 10 + private var cancellables: Set = [] override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. @@ -17,16 +135,432 @@ class StationViewModelTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. + func test_bindStationInfo_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 stationInfo가 정상적으로 왔는지 확인") + let expectationResult = StationDTO(stationID: 1, arsID: "1", stationName: "station1") + + // when + stationViewModel.$stationInfo + .sink(receiveValue: { [weak expectation] _ in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: timeout) + + // then + XCTAssertEqual(stationViewModel.stationInfo?.arsID, expectationResult.arsID) + XCTAssertEqual(stationViewModel.stationInfo?.stationName, expectationResult.stationName) + XCTAssertEqual(stationViewModel.stationInfo?.stationID, expectationResult.stationID) } - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. + func test_bindStationInfo_일치하는_Station_정보가_없는_경우() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.failure), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "일치하는 stationInfo가 없는 경우 에러를 반환하는지 확인") + var error: Error? = nil + + // when + stationViewModel.$error + .sink(receiveCompletion: { _ in + return + }, receiveValue: { [weak expectation] result in + error = result + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: timeout) + + // then + XCTAssertNotNil(error) + } + + func test_bindFavoriteItems_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 favoriteItems가 정상적으로 왔는지 확인") + let expectationResult = [FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1"), + FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1")] + + // when + stationViewModel.$favoriteItems + .compactMap({$0}) + .sink(receiveCompletion: { _ in + return + }, receiveValue: { [weak expectation] _ in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: timeout) + + // then + XCTAssertEqual(stationViewModel.favoriteItems?.count ?? 0, expectationResult.count) + stationViewModel.favoriteItems?.enumerated().forEach({ index, favoriteItem in + XCTAssertEqual(favoriteItem.arsId, expectationResult[index].arsId) + XCTAssertEqual(favoriteItem.busRouteId, expectationResult[index].busRouteId) + XCTAssertEqual(favoriteItem.ord, expectationResult[index].ord) + XCTAssertEqual(favoriteItem.stId, expectationResult[index].stId) + }) + } + + func test_bindFavoriteItems_빈_배열_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "0") + let expectation = self.expectation(description: "StationViewModel에 favoriteItems가 빈배열로 정상적으로 왔는지 확인") + + // when + stationViewModel.$favoriteItems + .compactMap({$0}) + .sink(receiveValue: { [weak expectation] _ in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: self.timeout) + + // then + XCTAssertNotNil(stationViewModel.favoriteItems) + XCTAssertEqual(stationViewModel.favoriteItems?.count ?? 0, 0) + } + + func test_refresh_nextStation_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 nextStation가 정상적으로 왔는지 확인") + let expectationResult = "송파와이즈더샵.엠코타운센트로엘" + var nextStation: String? = nil + + // when + stationViewModel.$nextStation + .compactMap({$0}) + .sink(receiveValue: { [weak expectation] result in + nextStation = result + expectation?.fulfill() + }) + .store(in: &self.cancellables) + stationViewModel.refresh() + + waitForExpectations(timeout: timeout) + + // then + XCTAssertEqual(nextStation, expectationResult) + } + + func test_refresh_nextStation_nil_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.failure), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 nextStation이 nil로 오는지 확인") + var nextStation: String? = nil + + stationViewModel.$nextStation + .output(at: 1) + .sink(receiveValue: { [weak expectation] result in + nextStation = result + expectation?.fulfill() + }) + .store(in: &self.cancellables) + stationViewModel.refresh() + + waitForExpectations(timeout: timeout) + + // then + XCTAssertNil(nextStation) + } + + + func test_refresh_activeBuses_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 activeBuses가 정상적으로 왔는지 확인") + let busArriveInfo: BusArriveInfo + busArriveInfo.nextStation = "송파와이즈더샵.엠코타운센트로엘" + busArriveInfo.busRouteId = 100100496 + busArriveInfo.busNumber = "333" + busArriveInfo.routeType = BBusRouteType.gansun + busArriveInfo.stationOrd = 7 + busArriveInfo.firstBusArriveRemainTime = BusRemainTime(arriveRemainTime: "2분20초") + busArriveInfo.firstBusRelativePosition = "1번째전" + busArriveInfo.firstBusCongestion = BusCongestion(rawValue: 0) + busArriveInfo.secondBusArriveRemainTime = BusRemainTime(arriveRemainTime: "7분20초") + busArriveInfo.secondBusRelativePosition = "3번째전" + busArriveInfo.secondBusCongestion = BusCongestion(rawValue: 0) + + let expectationResult = [BBusRouteType.gansun: BusArriveInfos(infos: [busArriveInfo])] + + // when + stationViewModel.$busKeys + .dropFirst() + .sink(receiveValue: { [weak expectation] a in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + stationViewModel.refresh() + + waitForExpectations(timeout: self.timeout) + + // then + XCTAssertEqual(stationViewModel.activeBuses.count, expectationResult.count) + stationViewModel.activeBuses.forEach({ key, info in + guard info.count() == expectationResult[key]?.count() ?? 0 else { return XCTFail() } + for i in 0..