diff --git a/iOS/Layover/Layover.xcodeproj/project.pbxproj b/iOS/Layover/Layover.xcodeproj/project.pbxproj index 2010426..cb582f5 100644 --- a/iOS/Layover/Layover.xcodeproj/project.pbxproj +++ b/iOS/Layover/Layover.xcodeproj/project.pbxproj @@ -188,6 +188,15 @@ FC4975932B03432800D8627F /* Pretendard-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FC49758A2B03432800D8627F /* Pretendard-Bold.ttf */; }; FC4975942B03432800D8627F /* Pretendard-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FC49758B2B03432800D8627F /* Pretendard-Regular.ttf */; }; FC4975992B03439000D8627F /* UIFont+.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4975982B03439000D8627F /* UIFont+.swift */; }; + FC4E0C0C2B282AE500152596 /* UploadPostViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C082B282AE500152596 /* UploadPostViewControllerTests.swift */; }; + FC4E0C0D2B282AE500152596 /* UploadPostInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C092B282AE500152596 /* UploadPostInteractorTests.swift */; }; + FC4E0C0E2B282AE500152596 /* UploadPostWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C0A2B282AE500152596 /* UploadPostWorkerTests.swift */; }; + FC4E0C0F2B282AE500152596 /* UploadPostPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C0B2B282AE500152596 /* UploadPostPresenterTests.swift */; }; + FC4E0C112B28595200152596 /* MockUploadPostWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C102B28595200152596 /* MockUploadPostWorker.swift */; }; + FC4E0C132B28609C00152596 /* PostBoard.json in Resources */ = {isa = PBXBuildFile; fileRef = FC4E0C122B28609C00152596 /* PostBoard.json */; }; + FC4E0C192B28955400152596 /* LocationFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C182B28955400152596 /* LocationFetcher.swift */; }; + FC4E0C1D2B28977000152596 /* CurrentLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C1C2B28977000152596 /* CurrentLocationManager.swift */; }; + FC4E0C202B28B4C500152596 /* MockLocationFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4E0C1F2B28B4C500152596 /* MockLocationFetcher.swift */; }; FC5BE11C2B148D160036366D /* EditProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC5BE1162B148D160036366D /* EditProfilePresenter.swift */; }; FC5BE11D2B148D160036366D /* EditProfileWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC5BE1172B148D160036366D /* EditProfileWorker.swift */; }; FC5BE11E2B148D160036366D /* EditProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC5BE1182B148D160036366D /* EditProfileRouter.swift */; }; @@ -432,6 +441,15 @@ FC49758A2B03432800D8627F /* Pretendard-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Bold.ttf"; sourceTree = ""; }; FC49758B2B03432800D8627F /* Pretendard-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Regular.ttf"; sourceTree = ""; }; FC4975982B03439000D8627F /* UIFont+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+.swift"; sourceTree = ""; }; + FC4E0C082B282AE500152596 /* UploadPostViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPostViewControllerTests.swift; sourceTree = ""; }; + FC4E0C092B282AE500152596 /* UploadPostInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPostInteractorTests.swift; sourceTree = ""; }; + FC4E0C0A2B282AE500152596 /* UploadPostWorkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPostWorkerTests.swift; sourceTree = ""; }; + FC4E0C0B2B282AE500152596 /* UploadPostPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPostPresenterTests.swift; sourceTree = ""; }; + FC4E0C102B28595200152596 /* MockUploadPostWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUploadPostWorker.swift; sourceTree = ""; }; + FC4E0C122B28609C00152596 /* PostBoard.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = PostBoard.json; sourceTree = ""; }; + FC4E0C182B28955400152596 /* LocationFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationFetcher.swift; sourceTree = ""; }; + FC4E0C1C2B28977000152596 /* CurrentLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentLocationManager.swift; sourceTree = ""; }; + FC4E0C1F2B28B4C500152596 /* MockLocationFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLocationFetcher.swift; sourceTree = ""; }; FC5BE1162B148D160036366D /* EditProfilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePresenter.swift; sourceTree = ""; }; FC5BE1172B148D160036366D /* EditProfileWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileWorker.swift; sourceTree = ""; }; FC5BE1182B148D160036366D /* EditProfileRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileRouter.swift; sourceTree = ""; }; @@ -595,6 +613,7 @@ 192513632B26F7BB001533FA /* TagPlayList */, 1925137B2B277CC4001533FA /* Profile */, 194C21BE2B1DEE5500C62645 /* Home */, + FCF1A02D2B275A0400D961BE /* UploadPost */, ); path = Scenes; sourceTree = ""; @@ -604,6 +623,7 @@ children = ( 194C21CE2B1DF63D00C62645 /* MockDatas */, 194C21CA2B1DF37900C62645 /* Workers */, + FC4E0C1E2B28B4AD00152596 /* LocationFetcher */, ); path = Mocks; sourceTree = ""; @@ -612,6 +632,7 @@ isa = PBXGroup; children = ( 194C21CB2B1DF39200C62645 /* MockHomeWorker.swift */, + FC4E0C102B28595200152596 /* MockUploadPostWorker.swift */, 1925136C2B26F84E001533FA /* MockTagPlayListWorker.swift */, 192513842B27852C001533FA /* MockUserWorker.swift */, 19AE481B2B28C53800DD4612 /* MockSettingWorker.swift */, @@ -622,6 +643,9 @@ 194C21CE2B1DF63D00C62645 /* MockDatas */ = { isa = PBXGroup; children = ( + 194C21D32B1EEE3700C62645 /* sample.jpeg */, + 194C21CF2B1DF65200C62645 /* PostList.json */, + FC4E0C122B28609C00152596 /* PostBoard.json */, 192513972B278645001533FA /* CheckSignUp.json */, 1925138D2B278645001533FA /* CheckUserName.json */, 192513932B278645001533FA /* DeleteUser.json */, @@ -909,6 +933,23 @@ path = Fonts; sourceTree = ""; }; + FC4E0C172B28954000152596 /* Location */ = { + isa = PBXGroup; + children = ( + FC4E0C182B28955400152596 /* LocationFetcher.swift */, + FC4E0C1C2B28977000152596 /* CurrentLocationManager.swift */, + ); + path = Location; + sourceTree = ""; + }; + FC4E0C1E2B28B4AD00152596 /* LocationFetcher */ = { + isa = PBXGroup; + children = ( + FC4E0C1F2B28B4C500152596 /* MockLocationFetcher.swift */, + ); + path = LocationFetcher; + sourceTree = ""; + }; FC5BE1112B148AF00036366D /* Views */ = { isa = PBXGroup; children = ( @@ -1030,6 +1071,7 @@ children = ( 1972CCD02B125E8800C3C762 /* UserDefaults */, 19C7AFD42B02583C003B35F2 /* Keychain */, + FC4E0C172B28954000152596 /* Location */, 1901D4A12B18F4BE00617A64 /* System.swift */, ); path = Services; @@ -1148,6 +1190,17 @@ path = SignUpScene; sourceTree = ""; }; + FCF1A02D2B275A0400D961BE /* UploadPost */ = { + isa = PBXGroup; + children = ( + FC4E0C082B282AE500152596 /* UploadPostViewControllerTests.swift */, + FC4E0C092B282AE500152596 /* UploadPostInteractorTests.swift */, + FC4E0C0A2B282AE500152596 /* UploadPostWorkerTests.swift */, + FC4E0C0B2B282AE500152596 /* UploadPostPresenterTests.swift */, + ); + path = UploadPost; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1265,6 +1318,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + FC4E0C132B28609C00152596 /* PostBoard.json in Resources */, 192513A02B278646001533FA /* PatchIntroduce.json in Resources */, 192513A32B278646001533FA /* sample.jpeg in Resources */, 1925139D2B278646001533FA /* PatchUserName.json in Resources */, @@ -1433,7 +1487,9 @@ 1915D6E52B1FB82000CE1DD0 /* CheckSignUpDTO.swift in Sources */, 198167A02B20583D0032F563 /* SettingPresenter.swift in Sources */, 198167A42B20583D0032F563 /* SettingInteractor.swift in Sources */, + FC4E0C1D2B28977000152596 /* CurrentLocationManager.swift in Sources */, 83C35E1B2B108C3500D8DD5C /* PlaybackView.swift in Sources */, + FC4E0C192B28955400152596 /* LocationFetcher.swift in Sources */, 835A61A02B068115002F22A5 /* PlaybackModels.swift in Sources */, FC0E80292B1A0BBB00EF56D6 /* UploadPostInteractor.swift in Sources */, 836C33912B17629400ECAFB0 /* MapRouter.swift in Sources */, @@ -1494,6 +1550,8 @@ 192513832B277CD7001533FA /* ProfilePresenterTests.swift in Sources */, 192513812B277CD7001533FA /* ProfileInteractorTests.swift in Sources */, 194C21C42B1DEE6B00C62645 /* HomeInteractorTests.swift in Sources */, + FC4E0C0F2B282AE500152596 /* UploadPostPresenterTests.swift in Sources */, + FC4E0C0C2B282AE500152596 /* UploadPostViewControllerTests.swift in Sources */, 1925136B2B26F7CE001533FA /* TagPlayListPresenterTests.swift in Sources */, 192513682B26F7CE001533FA /* TagPlayListViewControllerTests.swift in Sources */, 1925136A2B26F7CE001533FA /* TagPlayListWorkerTests.swift in Sources */, @@ -1503,6 +1561,7 @@ 19AE481C2B28C53800DD4612 /* MockSettingWorker.swift in Sources */, 192513802B277CD7001533FA /* ProfileViewControllerTests.swift in Sources */, 194C21C62B1DEE6B00C62645 /* HomePresenterTests.swift in Sources */, + FC4E0C202B28B4C500152596 /* MockLocationFetcher.swift in Sources */, 1925137A2B273D98001533FA /* StubAuthManager.swift in Sources */, 1925136D2B26F84E001533FA /* MockTagPlayListWorker.swift in Sources */, 19AE481A2B28C2B700DD4612 /* SettingPresenterTests.swift in Sources */, @@ -1510,6 +1569,9 @@ 194C21C32B1DEE6B00C62645 /* HomeViewControllerTests.swift in Sources */, 192513692B26F7CE001533FA /* TagPlayListInteractorTests.swift in Sources */, 194C21CC2B1DF39200C62645 /* MockHomeWorker.swift in Sources */, + FC4E0C0E2B282AE500152596 /* UploadPostWorkerTests.swift in Sources */, + FC4E0C112B28595200152596 /* MockUploadPostWorker.swift in Sources */, + FC4E0C0D2B282AE500152596 /* UploadPostInteractorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift index a31a522..fe655d2 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift @@ -18,7 +18,7 @@ final class UploadPostConfigurator: Configurator { func configure(_ viewController: ViewController) { let viewController = viewController - let interactor = UploadPostInteractor() + let interactor = UploadPostInteractor(locationManager: CurrentLocationManager()) let presenter = UploadPostPresenter() let router = UploadPostRouter() let worker = UploadPostWorker() diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift index 226cc63..0a4a956 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift @@ -15,15 +15,10 @@ import OSLog protocol UploadPostBusinessLogic { func fetchTags() func editTags(with request: UploadPostModels.EditTags.Request) - - @discardableResult - func fetchThumbnailImage() -> Task - - func fetchCurrentAddress() + func fetchThumbnailImage() async + func fetchCurrentAddress() async func canUploadPost(request: UploadPostModels.CanUploadPost.Request) - - @discardableResult - func uploadPost(request: UploadPostModels.UploadPost.Request) -> Task + func uploadPost(request: UploadPostModels.UploadPost.Request) } protocol UploadPostDataStore { @@ -42,7 +37,7 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD var presenter: UploadPostPresentationLogic? private let fileManager: FileManager - private let locationManager: CLLocationManager + private let locationManager: CurrentLocationManager // MARK: - Data Store @@ -53,7 +48,7 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD // MARK: - Object LifeCycle init(fileManager: FileManager = FileManager.default, - locationManager: CLLocationManager = CLLocationManager()) { + locationManager: CurrentLocationManager = CurrentLocationManager()) { self.fileManager = fileManager self.locationManager = locationManager } @@ -69,46 +64,38 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD tags = request.tags } - @discardableResult - func fetchThumbnailImage() -> Task { - Task { - guard let videoURL else { return false } + func fetchThumbnailImage() async { + guard let videoURL else { return } let asset = AVAsset(url: videoURL) let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true - do { - let image = try await generator.image(at: .zero).image - await MainActor.run { - presenter?.presentThumbnailImage(with: Models.FetchThumbnail.Response(thumbnailImage: image)) - } - return true - } catch let error { - os_log(.error, log: .default, "Failed to fetch Thumbnail Image with error: %@", error.localizedDescription) - return false + do { + let image = try await generator.image(at: .zero).image + await MainActor.run { + presenter?.presentThumbnailImage(with: Models.FetchThumbnail.Response(thumbnailImage: image)) } + } catch let error { + os_log(.error, log: .data, "Failed to fetch Thumbnail Image with error: %@", error.localizedDescription) } } - func fetchCurrentAddress() { - guard let location = getCurrentLocation() else { return } + func fetchCurrentAddress() async { + guard let location = locationManager.getCurrentLocation() else { return } let localeIdentifier = Locale.preferredLanguages.first != nil ? Locale.preferredLanguages[0] : Locale.current.identifier let locale = Locale(identifier: localeIdentifier) - - Task { - do { - let address = try await CLGeocoder().reverseGeocodeLocation(location, preferredLocale: locale).last - let administrativeArea = address?.administrativeArea - let locality = address?.locality - let subLocality = address?.subLocality - let response = Models.FetchCurrentAddress.Response(administrativeArea: administrativeArea, - locality: locality, - subLocality: subLocality) - await MainActor.run { - presenter?.presentCurrentAddress(with: response) - } - } catch { - os_log(.error, log: .default, "Failed to fetch Current Address with error: %@", error.localizedDescription) + do { + let address = try await CLGeocoder().reverseGeocodeLocation(location, preferredLocale: locale).last + let administrativeArea = address?.administrativeArea + let locality = address?.locality + let subLocality = address?.subLocality + let response = Models.FetchCurrentAddress.Response(administrativeArea: administrativeArea, + locality: locality, + subLocality: subLocality) + await MainActor.run { + presenter?.presentCurrentAddress(with: response) } + } catch { + os_log(.error, log: .data, "Failed to fetch Current Address with error: %@", error.localizedDescription) } } @@ -117,35 +104,28 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD presenter?.presentUploadButton(with: response) } - @discardableResult - func uploadPost(request: UploadPostModels.UploadPost.Request) -> Task { + func uploadPost(request: UploadPostModels.UploadPost.Request) { + guard let worker, + let videoURL, + let isMuted, + let coordinate = locationManager.getCurrentLocation()?.coordinate else { return } + if isMuted { + exportVideoWithoutAudio(at: videoURL) + } Task { - guard let worker, - let videoURL, - let isMuted, - let coordinate = getCurrentLocation()?.coordinate else { return false } - if isMuted { - exportVideoWithoutAudio(at: videoURL) - } - let response = await worker.uploadPost(with: UploadPost(title: request.title, - content: request.content, - latitude: coordinate.latitude, - longitude: coordinate.longitude, - tag: request.tags, - videoURL: videoURL)) - return response + let uploadPostResponse = await worker.uploadPost(with: UploadPost(title: request.title, + content: request.content, + latitude: coordinate.latitude, + longitude: coordinate.longitude, + tag: request.tags, + videoURL: videoURL)) + guard let boardID = uploadPostResponse?.id else { return } + let fileType = videoURL.pathExtension + _ = await worker.uploadVideo(with: UploadVideoRequestDTO(boardID: boardID, filetype: fileType), + videoURL: videoURL) } } - private func getCurrentLocation() -> CLLocation? { - locationManager.desiredAccuracy = kCLLocationAccuracyBest - locationManager.startUpdatingLocation() - - guard let space = locationManager.location?.coordinate else { return nil } - let location = CLLocation(latitude: space.latitude, longitude: space.longitude) - return location - } - private func exportVideoWithoutAudio(at url: URL) { let composition = AVMutableComposition() let sourceAsset = AVURLAsset(url: url) @@ -159,8 +139,8 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD let timeRange: CMTimeRange = CMTimeRangeMake(start: .zero, duration: sourceAssetduration) try compositionVideoTrack.insertTimeRange(timeRange, - of: sourceVideoTrack, - at: .zero) + of: sourceVideoTrack, + at: .zero) if fileManager.fileExists(atPath: url.path()) { try fileManager.removeItem(at: url) diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift index 264fe48..917387e 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift @@ -141,8 +141,7 @@ final class UploadPostViewController: BaseViewController { setConstraints() setDelegation() addTarget() - interactor?.fetchThumbnailImage() - interactor?.fetchCurrentAddress() + fetchPostInfo() } override func viewWillAppear(_ animated: Bool) { @@ -174,6 +173,13 @@ final class UploadPostViewController: BaseViewController { setContentViewSubviewsConstraints() } + private func fetchPostInfo() { + Task { + await interactor?.fetchCurrentAddress() + await interactor?.fetchThumbnailImage() + } + } + private func setDelegation() { titleTextField.delegate = self contentTextView.delegate = self diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift index 8d91918..02535f0 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift @@ -11,7 +11,8 @@ import UIKit import OSLog protocol UploadPostWorkerProtocol { - func uploadPost(with request: UploadPost) async -> Bool + func uploadPost(with request: UploadPost) async -> UploadPostDTO? + func uploadVideo(with request: UploadVideoRequestDTO, videoURL: URL) async -> Bool } final class UploadPostWorker: NSObject, UploadPostWorkerProtocol { @@ -30,7 +31,7 @@ final class UploadPostWorker: NSObject, UploadPostWorkerProtocol { self.uploadPostEndPointFactory = uploadPostEndPointFactory } - func uploadPost(with request: UploadPost) async -> Bool { + func uploadPost(with request: UploadPost) async -> UploadPostDTO? { let endPoint = uploadPostEndPointFactory.makeUploadPostEndPoint(title: request.title, content: request.content, latitude: request.latitude, @@ -38,26 +39,22 @@ final class UploadPostWorker: NSObject, UploadPostWorkerProtocol { tag: request.tag) do { let response = try await provider.request(with: endPoint) - guard let boardID = response.data?.id else { return false } - let fileType = request.videoURL.pathExtension - let uploadResponse = await uploadVideo(with: UploadVideoRequestDTO(boardID: boardID, filetype: fileType), - videoURL: request.videoURL) - return uploadResponse + return response.data } catch { os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) - return false + return nil } } - private func uploadVideo(with request: UploadVideoRequestDTO, videoURL: URL) async -> Bool { + func uploadVideo(with request: UploadVideoRequestDTO, videoURL: URL) async -> Bool { let endPoint = uploadPostEndPointFactory.makeUploadVideoEndPoint(boardID: request.boardID, fileType: request.filetype) do { let response = try await provider.request(with: endPoint) guard let preSignedURLString = response.data?.preSignedURL else { return false } _ = try await provider.upload(fromFile: videoURL, - to: preSignedURLString, - sessionTaskDelegate: self) + to: preSignedURLString, + sessionTaskDelegate: self) NotificationCenter.default.post(name: .uploadTaskDidComplete, object: nil) return true } catch { diff --git a/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift b/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift new file mode 100644 index 0000000..7649be4 --- /dev/null +++ b/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift @@ -0,0 +1,59 @@ +// +// CurrentLocationManager.swift +// Layover +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import CoreLocation + +final class CurrentLocationManager: NSObject { + + typealias LocationCompletion = ((CLLocation) -> Void) + + private var locationFetcher: LocationFetcher + var currentLocationCompletion: LocationCompletion? + var authrizationCompletion: ((CLAuthorizationStatus) -> Void)? + + init(locationFetcher: LocationFetcher = CLLocationManager()) { + self.locationFetcher = locationFetcher + super.init() + self.locationFetcher.locationFetcherDelegate = self + self.locationFetcher.desiredAccuracy = kCLLocationAccuracyBest + } + + func getCurrentLocation() -> CLLocation? { + startUpdatingLocation() + guard let space = locationFetcher.location?.coordinate else { return nil } + return CLLocation(latitude: space.latitude, longitude: space.longitude) + } + + func startUpdatingLocation() { + self.locationFetcher.startUpdatingLocation() + } + +} + +extension CurrentLocationManager: LocationFetcherDelegate { + func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.first else { return } + self.currentLocationCompletion?(location) + self.currentLocationCompletion = nil + } + + func locationFetcher(_ fetcher: LocationFetcher, didChangeAuthorization authorization: CLAuthorizationStatus) { + self.authrizationCompletion?(authorization) + self.authrizationCompletion = nil + } +} + +extension CurrentLocationManager: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + self.locationFetcher(manager, didUpdateLocations: locations) + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + self.locationFetcher(manager, didChangeAuthorization: manager.authorizationStatus) + } +} diff --git a/iOS/Layover/Layover/Services/Location/LocationFetcher.swift b/iOS/Layover/Layover/Services/Location/LocationFetcher.swift new file mode 100644 index 0000000..a76535e --- /dev/null +++ b/iOS/Layover/Layover/Services/Location/LocationFetcher.swift @@ -0,0 +1,36 @@ +// +// LocationFetcher.swift +// Layover +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import CoreLocation + +protocol LocationFetcherDelegate: AnyObject { + func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locations: [CLLocation]) + func locationFetcher(_ fetcher: LocationFetcher, didChangeAuthorization authorization: CLAuthorizationStatus) +} + +protocol LocationFetcher { + var location: CLLocation? { get } + var locationFetcherDelegate: LocationFetcherDelegate? { get set } + var desiredAccuracy: CLLocationAccuracy { get set } + + func requestLocation() + func startUpdatingLocation() + func requestWhenInUseAuthorization() + func requestAlwaysAuthorization() +} + +extension CLLocationManager: LocationFetcher { + var locationFetcherDelegate: LocationFetcherDelegate? { + get { + return delegate as? LocationFetcherDelegate + } + set { + delegate = newValue as? CLLocationManagerDelegate + } + } +} diff --git a/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift b/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift new file mode 100644 index 0000000..98481c0 --- /dev/null +++ b/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift @@ -0,0 +1,29 @@ +// +// MockLocationFetcher.swift +// LayoverTests +// +// Created by kong on 2023/12/13. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import CoreLocation + +@testable import Layover + +final class MockLocationFetcher: LocationFetcher { + var location: CLLocation? + var locationFetcherDelegate: Layover.LocationFetcherDelegate? + var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest + + func requestLocation() { } + + func startUpdatingLocation() { + location = CLLocation(latitude: 37.498206, longitude: 127.02761) + guard let location else { return } + locationFetcherDelegate?.locationFetcher(self, didUpdateLocations: [location]) + } + + func requestWhenInUseAuthorization() { } + + func requestAlwaysAuthorization() { } +} diff --git a/iOS/Layover/LayoverTests/Mocks/MockDatas/PostBoard.json b/iOS/Layover/LayoverTests/Mocks/MockDatas/PostBoard.json new file mode 100644 index 0000000..c4611d0 --- /dev/null +++ b/iOS/Layover/LayoverTests/Mocks/MockDatas/PostBoard.json @@ -0,0 +1,16 @@ +{ + "message": "성공", + "statusCode": 200, + "data": { + "id": 1, + "title": "제목", + "content": "내용", + "latitude": 37.1231053, + "longitude": 127.1231053, + "tag": [ + "부산", + "광안리", + "바다" + ] + } +} diff --git a/iOS/Layover/LayoverTests/Mocks/Workers/MockUploadPostWorker.swift b/iOS/Layover/LayoverTests/Mocks/Workers/MockUploadPostWorker.swift new file mode 100644 index 0000000..d74df6c --- /dev/null +++ b/iOS/Layover/LayoverTests/Mocks/Workers/MockUploadPostWorker.swift @@ -0,0 +1,56 @@ +// +// MockUploadPostWorker.swift +// LayoverTests +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation +import OSLog + +@testable import Layover + +final class MockUploadPostWorker: UploadPostWorkerProtocol { + + private let provider: ProviderType + + init(provider: ProviderType = Provider(session: .initMockSession(), + authManager: StubAuthManager())) { + self.provider = provider + } + + func uploadPost(with request: UploadPost) async -> UploadPostDTO? { + guard let fileLocation = Bundle(for: type(of: self)).url(forResource: "PostBoard", + withExtension: "json") else { return nil } + do { + let endPoint: EndPoint> = EndPoint(path: "/board", + method: .POST, + bodyParameters: UploadPostRequestDTO(title: request.title, + content: request.content, + latitude: request.latitude, + longitude: request.longitude, + tag: request.tag)) + let mockData = try Data(contentsOf: fileLocation) + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + let response = try await provider.request(with: endPoint) + guard let data = response.data else { return nil } + return data + } catch { + os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) + return nil + } + + } + + func uploadVideo(with request: UploadVideoRequestDTO, videoURL: URL) async -> Bool { + return true + } + +} diff --git a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift new file mode 100644 index 0000000..d508455 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift @@ -0,0 +1,202 @@ +// +// UploadPostInteractorTests.swift +// LayoverTests +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +@testable import Layover +import XCTest + +class UploadPostInteractorTests: XCTestCase { + + // MARK: - Subject Under Test (SUT) + + typealias Models = UploadPostModels + var sut: UploadPostInteractor! + + // MARK: - Test Lifecycle + + override func setUp() { + super.setUp() + setupUploadPostInteractor() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Test Setup + + func setupUploadPostInteractor() { + sut = UploadPostInteractor(locationManager: CurrentLocationManager(locationFetcher: MockLocationFetcher())) + sut.worker = MockUploadPostWorker(provider: Provider(session: .initMockSession())) + } + + // MARK: - Test Doubles + + class UploadPostPresentationLogicSpy: UploadPostPresentationLogic { + + // MARK: Spied Methods + + var presentTagCalled = false + var presentTagsResponse: UploadPostModels.FetchTags.Response! + func presentTags(with response: Layover.UploadPostModels.FetchTags.Response) { + presentTagCalled = true + presentTagsResponse = response + } + + var presentThumbnailCalled = false + var presentThumbnailResponse: UploadPostModels.FetchThumbnail.Response! + func presentThumbnailImage(with response: Layover.UploadPostModels.FetchThumbnail.Response) { + presentThumbnailCalled = true + presentThumbnailResponse = response + } + + var presentCurrentAddressCalled = false + var presentCurrentAddressResponse: UploadPostModels.FetchCurrentAddress.Response! + func presentCurrentAddress(with response: Layover.UploadPostModels.FetchCurrentAddress.Response) { + presentCurrentAddressCalled = true + presentCurrentAddressResponse = response + } + + var presentUploadButtonCalled = false + var presentUploadButtonResponse: UploadPostModels.CanUploadPost.Response! + func presentUploadButton(with response: Layover.UploadPostModels.CanUploadPost.Response) { + presentUploadButtonCalled = true + presentUploadButtonResponse = response + } + + } + + // MARK: - Tests + + func test_fetchTags를_호출하면_presenter의_presentTags가_호출된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + + // when + sut.fetchTags() + + // then + XCTAssertTrue(spy.presentTagCalled, "fetchTags 함수가 presentTags을 호출하지 못함") + } + + func test_fetchTags를_호출하면_datastore에_tags데이터가_전달된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + + // when + sut.fetchTags() + + // then + XCTAssertNotNil(sut.tags, "fetchTags 함수가 datastore에 tags 데이터를 전달하지 못함") + } + + func test_fetchCurrentAddress를_호출하면_presenter의_presentCurrentAddress가_호출된다() async { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + + // when + await sut.fetchCurrentAddress() + + // then + XCTAssertTrue(spy.presentCurrentAddressCalled, "fetchCurrentAddress 함수가 presentCurrentAddress을 호출하지 못함") + } + + func test_fetchCurrentAddress를_호출하면_presenter에게_위치데이터를_전달한다() async throws { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + + // when + await sut.fetchCurrentAddress() + + // then + XCTAssertNotNil(spy.presentCurrentAddressResponse, "fetchCurrentAddress 함수가 presenter에게 위치데이터를 전달하지 못함") + } + + func test_fetchThumbnailImage를_호출하면_presenter의_presentThumbnailImage가_호출된다() async { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + sut.videoURL = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8") + + // when + await sut.fetchThumbnailImage() + + // then + XCTAssertTrue(spy.presentThumbnailCalled, "fetchThumbnailImage 함수가 presentThumbnailImage을 호출하지 못함") + } + + func test_editTags를_호출하면_datastore에_tags데이터가_전달된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + let request = UploadPostModels.EditTags.Request(tags: []) + + // when + sut.editTags(with: request) + + // then + XCTAssertNotNil(sut.tags, "editTags 함수가 datastore의 tags 데이터를 전달하지 못함") + } + + func test_title이_nil일때_canUploadPost를_호출하면_presenter의_presentUploadButtonCalled가_호출된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + let request = UploadPostModels.CanUploadPost.Request(title: nil) + + // when + sut.canUploadPost(request: request) + + // then + XCTAssertTrue(spy.presentUploadButtonCalled, "title이 nil일 때 canUploadPost 함수가 presentUploadButton을 호출하지 못함") + } + + func test_title이_nil이아닐때_canUploadPost를_호출하면_presenter의_presentUploadButtonCalled가_호출된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + let request = UploadPostModels.CanUploadPost.Request(title: "아아아아") + + // when + sut.canUploadPost(request: request) + + // then + XCTAssertTrue(spy.presentUploadButtonCalled, "title이 nil이 아닐 때 canUploadPost 함수가 presentUploadButton을 호출하지 못함") + } + + func test_title이_nil일때_canUploadPost를_호출하면_올바른결과가_presenter에_전달된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + let request = UploadPostModels.CanUploadPost.Request(title: nil) + + // when + sut.canUploadPost(request: request) + + // then + XCTAssertEqual(spy.presentUploadButtonResponse.isEmpty, true, "title이 nil일 때 canUploadPost 함수가 올바른 결과를 전달하지 못함") + } + + func test_title이_nil이아닐때_canUploadPost를_호출하면_올바른결과가_presenter에_전달된다() { + // given + let spy = UploadPostPresentationLogicSpy() + sut.presenter = spy + let request = UploadPostModels.CanUploadPost.Request(title: "아아아아") + + // when + sut.canUploadPost(request: request) + + // then + XCTAssertEqual(spy.presentUploadButtonResponse.isEmpty, false, "title이 nil이 아닐 때 canUploadPost 함수가 올바른 결과를 전달하지 못함") + } + +} diff --git a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift new file mode 100644 index 0000000..99cef62 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift @@ -0,0 +1,131 @@ +// +// UploadPostPresenterTests.swift +// LayoverTests +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +@testable import Layover +import XCTest + +class UploadPostPresenterTests: XCTestCase { + + // MARK: - Subject Under Test (SUT) + + typealias Models = UploadPostModels + var sut: UploadPostPresenter! + + // MARK: - Test Lifecycle + + override func setUp() { + super.setUp() + setupUploadPostPresenter() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Test Setup + + func setupUploadPostPresenter() { + sut = UploadPostPresenter() + } + + // MARK: - Test Doubles + + class UploadPostDisplayLogicSpy: UploadPostDisplayLogic { + + // MARK: Spied Methods + + var displayTagsCalled = false + var displayTagsViewModel: UploadPostModels.FetchTags.ViewModel! + func displayTags(viewModel: Layover.UploadPostModels.FetchTags.ViewModel) { + displayTagsCalled = true + displayTagsViewModel = viewModel + + } + + var displayThumbnailCalled = false + var displayThumbnailViewModel: UploadPostModels.FetchThumbnail.ViewModel! + func displayThumbnail(viewModel: Layover.UploadPostModels.FetchThumbnail.ViewModel) { + displayThumbnailCalled = true + displayThumbnailViewModel = viewModel + } + + var displayCurrentAddressCalled = false + var displayCurrentAddressViewModel: UploadPostModels.FetchCurrentAddress.ViewModel! + func displayCurrentAddress(viewModel: Layover.UploadPostModels.FetchCurrentAddress.ViewModel) { + displayCurrentAddressCalled = true + displayCurrentAddressViewModel = viewModel + } + + var displayUploadButtonCalled = false + var displayUploadButtonViewModel: UploadPostModels.CanUploadPost.ViewModel! + func displayUploadButton(viewModel: Layover.UploadPostModels.CanUploadPost.ViewModel) { + displayUploadButtonCalled = true + displayUploadButtonViewModel = viewModel + } + + } + + // MARK: - Tests + + func test_presentTags를_호출하면_displayTags를_호출한다() { + // given + let spy = UploadPostDisplayLogicSpy() + sut.viewController = spy + let response = Models.FetchTags.Response(tags: []) + + // when + sut.presentTags(with: response) + + // then + XCTAssertTrue(spy.displayTagsCalled, "presentTags(with:) 함수가 displayTags를 호출하지 못함") + } + + func test_presentThumbnail을_호출하면_displayThumbnail을_호출한다() { + // given + let spy = UploadPostDisplayLogicSpy() + sut.viewController = spy + let thumbnailImage = UIImage(resource: .content).cgImage! + let response = Models.FetchThumbnail.Response(thumbnailImage: thumbnailImage) + + // when + sut.presentThumbnailImage(with: response) + + // then + XCTAssertTrue(spy.displayThumbnailCalled, "presentThumbnailImage(with:) 함수가 displayThumbnail을 호출하지 못함") + } + + func test_presentCurrentAddress를_호출하면_displayCurrentAddress를_호출한다() { + // given + let spy = UploadPostDisplayLogicSpy() + sut.viewController = spy + let response = Models.FetchCurrentAddress.Response(administrativeArea: nil, + locality: nil, + subLocality: nil) + + // when + sut.presentCurrentAddress(with: response) + + // then + XCTAssertTrue(spy.displayCurrentAddressCalled, "presentCurrentAddress(with:) 함수가 displayCurrentAddress을 호출하지 못함") + } + + func test_presentUploadButton를_호출하면_displayUploadButton을_호출한다() { + // given + let spy = UploadPostDisplayLogicSpy() + sut.viewController = spy + let response = Models.CanUploadPost.Response(isEmpty: true) + + // when + sut.presentUploadButton(with: response) + + // then + XCTAssertTrue(spy.displayUploadButtonCalled, "presentUploadButton(with:) 함수가 displayUploadButton을 호출하지 못함") + } + +} diff --git a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostViewControllerTests.swift b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostViewControllerTests.swift new file mode 100644 index 0000000..3cd99c9 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostViewControllerTests.swift @@ -0,0 +1,203 @@ +// +// UploadPostViewControllerTests.swift +// LayoverTests +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +@testable import Layover +import XCTest + +class UploadPostViewControllerTests: XCTestCase { + + // MARK: - Subject Under Test (SUT) + + typealias Models = UploadPostModels + var sut: UploadPostViewController! + var window: UIWindow! + + // MARK: - Test Lifecycle + + override func setUp() { + super.setUp() + window = UIWindow() + setupUploadPostViewController() + } + + override func tearDown() { + window = nil + sut = nil + super.tearDown() + } + + // MARK: - Test Setup + + func setupUploadPostViewController() { + let bundle = Bundle.main + let storyboard = UIStoryboard(name: "Main", bundle: bundle) + sut = storyboard.instantiateViewController(withIdentifier: "UploadPostViewController") as? UploadPostViewController + } + + func loadView() { + window.addSubview(sut.view) + RunLoop.current.run(until: Date()) + } +// +// // MARK: - Test Doubles +// +// class UploadPostBusinessLogicSpy: UploadPostBusinessLogic { +// +// // MARK: Spied Methods +// +// var fetchFromLocalDataStoreCalled = false +// func fetchFromLocalDataStore(with request: UploadPostModels.FetchFromLocalDataStore.Request) { +// fetchFromLocalDataStoreCalled = true +// } +// +// var fetchFromRemoteDataStoreCalled = false +// func fetchFromRemoteDataStore(with request: UploadPostModels.FetchFromRemoteDataStore.Request) { +// fetchFromRemoteDataStoreCalled = true +// } +// +// var trackAnalyticsCalled = false +// func trackAnalytics(with request: UploadPostModels.TrackAnalytics.Request) { +// trackAnalyticsCalled = true +// } +// +// var performUploadPostCalled = false +// func performUploadPost(with request: UploadPostModels.PerformUploadPost.Request) { +// performUploadPostCalled = true +// } +// } +// +// class UploadPostRouterSpy: UploadPostRouter { +// +// // MARK: Spied Methods +// +// var routeToNextCalled = false +// override func routeToNext() { +// routeToNextCalled = true +// } +// } +// +// // MARK: - Tests +// +// func testShouldFetchFromLocalDataStoreWhenViewIsLoaded() { +// // given +// let spy = UploadPostBusinessLogicSpy() +// sut.interactor = spy +// +// // when +// loadView() +// +// // then +// XCTAssertTrue(spy.fetchFromLocalDataStoreCalled, "viewDidLoad() should ask the interactor to fetch from local DataStore") +// } +// +// func testShouldDisplayDataFetchedFromLocalDataStore() { +// // given +// loadView() +// let translation = "Example string." +// let viewModel = Models.FetchFromLocalDataStore.ViewModel(exampleTranslation: translation) +// +// // when +// sut.displayFetchFromLocalDataStore(with: viewModel) +// +// // then +// XCTAssertEqual(sut.exampleLocalLabel.text, translation, "displayFetchFromLocalDataStore(with:) should display the correct example label text") +// } +// +// func testShouldFetchFromRemoteDataStoreWhenViewWillAppear() { +// // given +// let spy = UploadPostBusinessLogicSpy() +// sut.interactor = spy +// +// // when +// loadView() +// +// // then +// XCTAssertTrue(spy.fetchFromRemoteDataStoreCalled, "viewWillAppear(_:) should ask the interactor to fetch from remote DataStore") +// } +// +// func testShouldDisplayDataFetchedFromRemoteDataStore() { +// // given +// loadView() +// let exampleVariable = "Example string." +// let viewModel = Models.FetchFromRemoteDataStore.ViewModel(exampleVariable: exampleVariable) +// +// // when +// sut.displayFetchFromRemoteDataStore(with: viewModel) +// +// // then +// XCTAssertEqual(sut.exampleRemoteLabel.text, exampleVariable, "displayFetchFromRemoteDataStore(with:) should display the correct example label text") +// } +// +// func testShouldTrackAnalyticsWhenViewDidAppear() { +// // given +// let spy = UploadPostBusinessLogicSpy() +// sut.interactor = spy +// loadView() +// +// // when +// sut.viewDidAppear(true) +// +// // then +// XCTAssertTrue(spy.trackAnalyticsCalled, "When needed, view controller should ask the interactor to track analytics") +// } +// +// func testShouldDisplayTrackAnalyticsWhenDisplayTrackAnalytics() { +// // given +// loadView() +// let viewModel = Models.TrackAnalytics.ViewModel() +// +// // when +// sut.displayTrackAnalytics(with: viewModel) +// +// // then +// // assert something here based on use case +// } +// +// func testUnsuccessfulUploadPostShouldShowErrorAsLabel() { +// // given +// loadView() +// var error = Models.UploadPostError(type: .emptyExampleVariable) +// error.message = "Example error" +// let viewModel = Models.PerformUploadPost.ViewModel(error: error) +// +// // when +// sut.displayPerformUploadPost(with: viewModel) +// +// // then +// XCTAssertEqual(sut.exampleLocalLabel.text, error.message, "displayPerformUploadPost(with:) should set error as label if there is an error") +// } +// +// func testUnsuccessfulUploadPostShouldNotRouteToNext() { +// // given +// let spy = UploadPostRouterSpy() +// sut.router = spy +// loadView() +// let error = UploadPostModels.Error.init(type: .emptyExampleVariable) +// let viewModel = UploadPostModels.PerformUploadPost.ViewModel(error: error) +// +// // when +// sut.displayPerformUploadPost(with: viewModel) +// +// // then +// XCTAssertFalse(spy.routeToNextCalled, "displayPerformUploadPost(with:) should not route to next screen if there is an error") +// } +// +// func testSuccessfulUploadPostShouldRouteToNext() { +// // given +// let spy = UploadPostRouterSpy() +// sut.router = spy +// loadView() +// let viewModel = UploadPostModels.PerformUploadPost.ViewModel(error: nil) +// +// // when +// sut.displayPerformUploadPost(with: viewModel) +// +// // then +// XCTAssertTrue(spy.routeToNextCalled, "displayPerformUploadPost(with:) should route to next screen if there is no error") +// } +} diff --git a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostWorkerTests.swift b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostWorkerTests.swift new file mode 100644 index 0000000..3ca5e52 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostWorkerTests.swift @@ -0,0 +1,69 @@ +// +// UploadPostWorkerTests.swift +// LayoverTests +// +// Created by kong on 2023/12/12. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +@testable import Layover +import XCTest + +class UploadPostWorkerTests: XCTestCase { + + // MARK: - Subject Under Test (SUT) + + typealias Models = UploadPostModels + var sut: UploadPostWorker! + + // MARK: - Test Lifecycle + + override func setUp() { + super.setUp() + setupUploadPostWorker() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Test Setup + + func setupUploadPostWorker() { + sut = UploadPostWorker(provider: Provider(session: .initMockSession(), authManager: StubAuthManager())) + } + + func test_uploadPost는_업로드에_성공하면_올바른결과를_반환한다() async { + // given + guard let mockFileLocation = Bundle(for: type(of: self)).url(forResource: "PostBoard", withExtension: "json"), + let mockData = try? Data(contentsOf: mockFileLocation) else { + XCTFail("Mock json 파일 로드 실패.") + return + } + + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + + let request = UploadPost(title: "제목", + content: nil, + latitude: 100, + longitude: 100, + tag: [], + videoURL: URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")!) + + // when + let response = await sut.uploadPost(with: request) +// try await Task.sleep(nanoseconds: 3_000_000_000) + + // then + XCTAssertNotNil(response, "uploadPost가 response를 정상적으로 반환하지 못함") + XCTAssertEqual(response?.id, 1) + } + +} diff --git a/iOS/Layover/LayoverTests/Seeds.swift b/iOS/Layover/LayoverTests/Seeds.swift index f64fb9a..294d99c 100644 --- a/iOS/Layover/LayoverTests/Seeds.swift +++ b/iOS/Layover/LayoverTests/Seeds.swift @@ -10,7 +10,7 @@ import Foundation // JSON 데이터와 비교하기 위한 모델 class Seeds { - static let sampleImageData = try! Data(contentsOf: Bundle(for: Seeds.self).url(forResource: "sample", withExtension: "jpeg")!) + static let sampleImageData = try? Data(contentsOf: Bundle(for: Seeds.self).url(forResource: "sample", withExtension: "jpeg")!) struct Posts { // PostList.json에 정의된 데이터