diff --git a/iOS/Layover/Layover.xcodeproj/project.pbxproj b/iOS/Layover/Layover.xcodeproj/project.pbxproj index fca1443..65b6a36 100644 --- a/iOS/Layover/Layover.xcodeproj/project.pbxproj +++ b/iOS/Layover/Layover.xcodeproj/project.pbxproj @@ -34,6 +34,13 @@ 1945523B2B05258200299768 /* HomeConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1945523A2B05258200299768 /* HomeConfigurator.swift */; }; 194C21BB2B1B718B00C62645 /* MockHomeWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21BA2B1B718B00C62645 /* MockHomeWorker.swift */; }; 194C21BD2B1B728600C62645 /* StubAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21BC2B1B728600C62645 /* StubAuthManager.swift */; }; + 194C21C32B1DEE6B00C62645 /* HomeViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21BF2B1DEE6A00C62645 /* HomeViewControllerTests.swift */; }; + 194C21C42B1DEE6B00C62645 /* HomeInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21C02B1DEE6A00C62645 /* HomeInteractorTests.swift */; }; + 194C21C52B1DEE6B00C62645 /* HomeWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21C12B1DEE6A00C62645 /* HomeWorkerTests.swift */; }; + 194C21C62B1DEE6B00C62645 /* HomePresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21C22B1DEE6A00C62645 /* HomePresenterTests.swift */; }; + 194C21CC2B1DF39200C62645 /* MockHomeWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C21CB2B1DF39200C62645 /* MockHomeWorker.swift */; }; + 194C21D02B1DF65200C62645 /* PostList.json in Resources */ = {isa = PBXBuildFile; fileRef = 194C21CF2B1DF65200C62645 /* PostList.json */; }; + 194C21D42B1EEE3700C62645 /* sample.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 194C21D32B1EEE3700C62645 /* sample.jpeg */; }; 1972CCCF2B12438900C3C762 /* LoginEndPointFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1972CCCE2B12438900C3C762 /* LoginEndPointFactory.swift */; }; 1972CCD22B125ED700C3C762 /* UserDefaultStored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1972CCD12B125ED700C3C762 /* UserDefaultStored.swift */; }; 1972CCD42B138E6B00C3C762 /* SignUpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1972CCD32B138E6B00C3C762 /* SignUpRouter.swift */; }; @@ -160,7 +167,6 @@ FC7E453C2AFEB623004F155A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7E453B2AFEB623004F155A /* SceneDelegate.swift */; }; FC7E45432AFEB62B004F155A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FC7E45422AFEB62B004F155A /* Assets.xcassets */; }; FC7E45462AFEB62B004F155A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC7E45442AFEB62B004F155A /* LaunchScreen.storyboard */; }; - FC7E45512AFEB62C004F155A /* LayoverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7E45502AFEB62C004F155A /* LayoverTests.swift */; }; FC7E456A2AFEC06E004F155A /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = FC7E45692AFEC06E004F155A /* .swiftlint.yml */; }; FC930E752B0CD75C00AA48E3 /* ProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC930E6F2B0CD75C00AA48E3 /* ProfilePresenter.swift */; }; FC930E772B0CD75C00AA48E3 /* ProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC930E712B0CD75C00AA48E3 /* ProfileRouter.swift */; }; @@ -214,6 +220,13 @@ 1945523A2B05258200299768 /* HomeConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeConfigurator.swift; sourceTree = ""; }; 194C21BA2B1B718B00C62645 /* MockHomeWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHomeWorker.swift; sourceTree = ""; }; 194C21BC2B1B728600C62645 /* StubAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubAuthManager.swift; sourceTree = ""; }; + 194C21BF2B1DEE6A00C62645 /* HomeViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewControllerTests.swift; sourceTree = ""; }; + 194C21C02B1DEE6A00C62645 /* HomeInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeInteractorTests.swift; sourceTree = ""; }; + 194C21C12B1DEE6A00C62645 /* HomeWorkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWorkerTests.swift; sourceTree = ""; }; + 194C21C22B1DEE6A00C62645 /* HomePresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePresenterTests.swift; sourceTree = ""; }; + 194C21CB2B1DF39200C62645 /* MockHomeWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHomeWorker.swift; sourceTree = ""; }; + 194C21CF2B1DF65200C62645 /* PostList.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = PostList.json; sourceTree = ""; }; + 194C21D32B1EEE3700C62645 /* sample.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = sample.jpeg; sourceTree = ""; }; 1972CCCE2B12438900C3C762 /* LoginEndPointFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginEndPointFactory.swift; sourceTree = ""; }; 1972CCD12B125ED700C3C762 /* UserDefaultStored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultStored.swift; sourceTree = ""; }; 1972CCD32B138E6B00C3C762 /* SignUpRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRouter.swift; sourceTree = ""; }; @@ -346,7 +359,6 @@ FC7E45452AFEB62B004F155A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; FC7E45472AFEB62B004F155A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FC7E454C2AFEB62B004F155A /* LayoverTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LayoverTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - FC7E45502AFEB62C004F155A /* LayoverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoverTests.swift; sourceTree = ""; }; FC7E45692AFEC06E004F155A /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; FC930E6F2B0CD75C00AA48E3 /* ProfilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePresenter.swift; sourceTree = ""; }; FC930E712B0CD75C00AA48E3 /* ProfileRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRouter.swift; sourceTree = ""; }; @@ -428,6 +440,51 @@ path = Cell; sourceTree = ""; }; + 194C21BE2B1DEE5500C62645 /* Home */ = { + isa = PBXGroup; + children = ( + 194C21BF2B1DEE6A00C62645 /* HomeViewControllerTests.swift */, + 194C21C02B1DEE6A00C62645 /* HomeInteractorTests.swift */, + 194C21C12B1DEE6A00C62645 /* HomeWorkerTests.swift */, + 194C21C22B1DEE6A00C62645 /* HomePresenterTests.swift */, + ); + path = Home; + sourceTree = ""; + }; + 194C21C72B1DF09B00C62645 /* Scenes */ = { + isa = PBXGroup; + children = ( + 194C21BE2B1DEE5500C62645 /* Home */, + ); + path = Scenes; + sourceTree = ""; + }; + 194C21C92B1DF36E00C62645 /* Mocks */ = { + isa = PBXGroup; + children = ( + 194C21CE2B1DF63D00C62645 /* MockDatas */, + 194C21CA2B1DF37900C62645 /* Workers */, + ); + path = Mocks; + sourceTree = ""; + }; + 194C21CA2B1DF37900C62645 /* Workers */ = { + isa = PBXGroup; + children = ( + 194C21CB2B1DF39200C62645 /* MockHomeWorker.swift */, + ); + path = Workers; + sourceTree = ""; + }; + 194C21CE2B1DF63D00C62645 /* MockDatas */ = { + isa = PBXGroup; + children = ( + 194C21D32B1EEE3700C62645 /* sample.jpeg */, + 194C21CF2B1DF65200C62645 /* PostList.json */, + ); + path = MockDatas; + sourceTree = ""; + }; 1972CCCD2B12436300C3C762 /* Factories */ = { isa = PBXGroup; children = ( @@ -764,7 +821,8 @@ FC7E454F2AFEB62C004F155A /* LayoverTests */ = { isa = PBXGroup; children = ( - FC7E45502AFEB62C004F155A /* LayoverTests.swift */, + 194C21C92B1DF36E00C62645 /* Mocks */, + 194C21C72B1DF09B00C62645 /* Scenes */, ); path = LayoverTests; sourceTree = ""; @@ -1000,6 +1058,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 194C21D02B1DF65200C62645 /* PostList.json in Resources */, + 194C21D42B1EEE3700C62645 /* sample.jpeg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1189,7 +1249,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FC7E45512AFEB62C004F155A /* LayoverTests.swift in Sources */, + 194C21C42B1DEE6B00C62645 /* HomeInteractorTests.swift in Sources */, + 194C21C52B1DEE6B00C62645 /* HomeWorkerTests.swift in Sources */, + 194C21C62B1DEE6B00C62645 /* HomePresenterTests.swift in Sources */, + 194C21C32B1DEE6B00C62645 /* HomeViewControllerTests.swift in Sources */, + 194C21CC2B1DF39200C62645 /* MockHomeWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS/Layover/Layover/Network/DTOs/BoardDTO.swift b/iOS/Layover/Layover/Network/DTOs/BoardDTO.swift index cf1b12d..0eca599 100644 --- a/iOS/Layover/Layover/Network/DTOs/BoardDTO.swift +++ b/iOS/Layover/Layover/Network/DTOs/BoardDTO.swift @@ -10,14 +10,14 @@ import Foundation struct BoardDTO: Decodable { let id: Int - let url: String - let videoThumbnail: String + let encodedVideoURL: String + let videoThumbnailURL: String let latitude, longitude, title, content: String enum CodingKeys: String, CodingKey { case id - case url = "url" - case videoThumbnail = "video_thumbnail" + case encodedVideoURL = "encoded_video_url" + case videoThumbnailURL = "video_thumbnail_url" case latitude, longitude, title, content } } @@ -28,8 +28,8 @@ extension BoardDTO { identifier: id, title: title, description: content, - thumbnailImageURL: URL(string: videoThumbnail), - videoURL: URL(string: url), + thumbnailImageURL: URL(string: videoThumbnailURL), + videoURL: URL(string: encodedVideoURL), latitude: Double(latitude), longitude: Double(longitude) ) diff --git a/iOS/Layover/Layover/Network/Mock/MockData/PostList.json b/iOS/Layover/Layover/Network/Mock/MockData/PostList.json index 490b7cc..4c187da 100644 --- a/iOS/Layover/Layover/Network/Mock/MockData/PostList.json +++ b/iOS/Layover/Layover/Network/Mock/MockData/PostList.json @@ -12,8 +12,8 @@ }, "board" : { "id" : 1, - "url" : "https://assets.afcdn.com/video49/20210722/v_645516.m3u8", - "video_thumbnail" : "https://think-note.com/wp-content/uploads/2023/07/eta_3.jpg", + "encoded_video_url" : "https://assets.afcdn.com/video49/20210722/v_645516.m3u8", + "video_thumbnail_url" : "https://think-note.com/wp-content/uploads/2023/07/eta_3.jpg", "longitude" : "37.0532156213", "latitude" : "127.060123123", "title" : "최강 아이돌", @@ -30,8 +30,8 @@ }, "board" : { "id" : 2, - "url" : "https://qc66zhsq1708.edge.naverncp.com/hls/fMG98EEU8c1UirV-awtm4qKJyhaHRcnDymRFlPLZbTs_/layover-station/IMG_0136_AVC_,HD,SD,_1Pass_30fps.mp4.smil/master.m3u8", - "video_thumbnail" : "video_thumbnail_link", + "encoded_video_url" : "https://qc66zhsq1708.edge.naverncp.com/hls/fMG98EEU8c1UirV-awtm4qKJyhaHRcnDymRFlPLZbTs_/layover-station/IMG_0136_AVC_,HD,SD,_1Pass_30fps.mp4.smil/master.m3u8", + "video_thumbnail_url" : "video_thumbnail_link", "longitude" : "37.0532156213", "latitude" : "127.060123123", "title" : "프로듀스 101", @@ -48,8 +48,8 @@ }, "board" : { "id" : 3, - "url" : "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", - "video_thumbnail" : "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize", + "encoded_video_url" : "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", + "video_thumbnail_url" : "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize", "longitude" : "37.0532156213", "latitude" : "127.060123123", "title" : "프로미스 나인", @@ -66,8 +66,8 @@ }, "board" : { "id" : 4, - "url" : "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", - "video_thumbnail" : "https://res.heraldm.com/content/image/2023/04/16/20230416000040_0.jpg", + "encoded_video_url" : "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + "video_thumbnail_url" : "https://res.heraldm.com/content/image/2023/04/16/20230416000040_0.jpg", "longitude" : "37.0532156213", "latitude" : "127.060123123", "title" : "아이즈원", diff --git a/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift b/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift index 36659ec..bc4c8e3 100644 --- a/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift +++ b/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift @@ -9,8 +9,10 @@ import UIKit protocol HomeBusinessLogic { - func fetchPosts(with request: HomeModels.FetchPosts.Request) - func fetchThumbnailImageData(with request: HomeModels.FetchThumbnailImageData.Request) + @discardableResult + func fetchPosts(with request: HomeModels.FetchPosts.Request) -> Task + @discardableResult + func fetchThumbnailImageData(with request: HomeModels.FetchThumbnailImageData.Request) -> Task func playPosts(with request: HomeModels.PlayPosts.Request) func selectVideo(with request: HomeModels.SelectVideo.Request) func showTagPlayList(with request: HomeModels.ShowTagPlayList.Request) @@ -45,27 +47,33 @@ final class HomeInteractor: HomeDataStore { // MARK: - Use Case extension HomeInteractor: HomeBusinessLogic { - func fetchPosts(with request: Models.FetchPosts.Request) { + @discardableResult + func fetchPosts(with request: Models.FetchPosts.Request) -> Task { Task { - guard let posts = await homeWorker?.fetchPosts() else { return } + guard let posts = await homeWorker?.fetchPosts() else { return false } let response = Models.FetchPosts.Response(posts: posts) await MainActor.run { self.posts = posts presenter?.presentPosts(with: response) } + + return true } } - func fetchThumbnailImageData(with request: HomeModels.FetchThumbnailImageData.Request) { + @discardableResult + func fetchThumbnailImageData(with request: HomeModels.FetchThumbnailImageData.Request) -> Task { Task { - guard let imageData = await homeWorker?.fetchImageData(of: request.imageURL) else { return } + guard let imageData = await homeWorker?.fetchImageData(of: request.imageURL) else { return false } let response = Models.FetchThumbnailImageData.Response(imageData: imageData, indexPath: request.indexPath) await MainActor.run { presenter?.presentThumbnailImage(with: response) } + + return true } } diff --git a/iOS/Layover/Layover/Scenes/Home/HomeRouter.swift b/iOS/Layover/Layover/Scenes/Home/HomeRouter.swift index 133846d..3438ec6 100644 --- a/iOS/Layover/Layover/Scenes/Home/HomeRouter.swift +++ b/iOS/Layover/Layover/Scenes/Home/HomeRouter.swift @@ -19,7 +19,7 @@ protocol HomeDataPassing { var dataStore: HomeDataStore? { get } } -final class HomeRouter: NSObject, HomeRoutingLogic, HomeDataPassing { +class HomeRouter: NSObject, HomeRoutingLogic, HomeDataPassing { // MARK: - Properties diff --git a/iOS/Layover/LayoverTests/LayoverTests.swift b/iOS/Layover/LayoverTests/LayoverTests.swift deleted file mode 100644 index 6eae0ec..0000000 --- a/iOS/Layover/LayoverTests/LayoverTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// LayoverTests.swift -// LayoverTests -// -// Created by kong on 2023/11/11. -// - -import XCTest -@testable import Layover - -final class LayoverTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // 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. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/iOS/Layover/LayoverTests/Mocks/MockDatas/PostList.json b/iOS/Layover/LayoverTests/Mocks/MockDatas/PostList.json new file mode 100644 index 0000000..4c187da --- /dev/null +++ b/iOS/Layover/LayoverTests/Mocks/MockDatas/PostList.json @@ -0,0 +1,79 @@ +{ + "customCode": "SUCCESS", + "message": "요청이 성공적으로 처리되었습니다.", + "statusCode": 200, + "data": [ + { + "member" : { + "id" : 1, + "username" : "loinsir", + "introduce" : "Hi, my name is hwani", + "profile_image_url" : "https://i.namu.wiki/i/HPFgEkrlX8wtU-agoxsqlzllzfJrcFkDSJYFQxdBHUNyjyZtUpS9zy-7-6lfz4ngzB-1wbLRyhIP4TmnwPwKJ0mUec5403r5TJnI3NZpsYJL6GEVQTmR52YoFGLMbaIe4aGSzh4B4InI9r2g0VV74g.svg" + }, + "board" : { + "id" : 1, + "encoded_video_url" : "https://assets.afcdn.com/video49/20210722/v_645516.m3u8", + "video_thumbnail_url" : "https://think-note.com/wp-content/uploads/2023/07/eta_3.jpg", + "longitude" : "37.0532156213", + "latitude" : "127.060123123", + "title" : "최강 아이돌", + "content" : "게시글 설명" + }, + "tag" : ["나도몰라요", "너도몰라요"] + }, + { + "member" : { + "id" : 2, + "username" : "chopmozzi", + "introduce" : "Hi, my name is hwani", + "profile_image_url" : "https://m.segye.com/content/image/2023/03/15/20230315514234.jpg" + }, + "board" : { + "id" : 2, + "encoded_video_url" : "https://qc66zhsq1708.edge.naverncp.com/hls/fMG98EEU8c1UirV-awtm4qKJyhaHRcnDymRFlPLZbTs_/layover-station/IMG_0136_AVC_,HD,SD,_1Pass_30fps.mp4.smil/master.m3u8", + "video_thumbnail_url" : "video_thumbnail_link", + "longitude" : "37.0532156213", + "latitude" : "127.060123123", + "title" : "프로듀스 101", + "content" : "게시글 설명" + }, + "tag" : ["해시태그", "해시태그2"] + }, + { + "member" : { + "id" : 3, + "username" : "anyukyung", + "introduce" : "Hi, my name is hwani", + "profile_image_url" : "https://images.khan.co.kr/article/2021/11/15/l_2021111502000877900178361.jpg" + }, + "board" : { + "id" : 3, + "encoded_video_url" : "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", + "video_thumbnail_url" : "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize", + "longitude" : "37.0532156213", + "latitude" : "127.060123123", + "title" : "프로미스 나인", + "content" : "게시글 설명" + }, + "tag" : ["해시태그1", "해시태그2"] + }, + { + "member" : { + "id" : 4, + "username" : "layover", + "introduce" : "Hi, my name is hwani", + "profile_image_url" : "profile_image_link" + }, + "board" : { + "id" : 4, + "encoded_video_url" : "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + "video_thumbnail_url" : "https://res.heraldm.com/content/image/2023/04/16/20230416000040_0.jpg", + "longitude" : "37.0532156213", + "latitude" : "127.060123123", + "title" : "아이즈원", + "content" : "게시글 설명2" + }, + "tag" : ["해시태그1", "해시태그6"] + } + ] +} diff --git a/iOS/Layover/LayoverTests/Mocks/MockDatas/sample.jpeg b/iOS/Layover/LayoverTests/Mocks/MockDatas/sample.jpeg new file mode 100644 index 0000000..a88d861 Binary files /dev/null and b/iOS/Layover/LayoverTests/Mocks/MockDatas/sample.jpeg differ diff --git a/iOS/Layover/LayoverTests/Mocks/Workers/MockHomeWorker.swift b/iOS/Layover/LayoverTests/Mocks/Workers/MockHomeWorker.swift new file mode 100644 index 0000000..866a143 --- /dev/null +++ b/iOS/Layover/LayoverTests/Mocks/Workers/MockHomeWorker.swift @@ -0,0 +1,68 @@ +// +// MockHomeWorker.swift +// LayoverTests +// +// Created by 김인환 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation +import OSLog + +@testable import Layover + +final class MockHomeWorker: HomeWorkerProtocol { + + // MARK: - Properties + + private let provider: ProviderType = Provider(session: .initMockSession(), + authManager: StubAuthManager()) + + // MARK: - Methods + + func fetchPosts() async -> [Post]? { + guard let fileLocation = Bundle(for: type(of: self)).url(forResource: "PostList", + withExtension: "json") else { return nil } + + do { + 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 endPoint: EndPoint = EndPoint>(path: "/board/home", + method: .GET) + let response = try await provider.request(with: endPoint) + guard let data = response.data else { return nil } + return data.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "%@", error.localizedDescription) + return nil + } + } + + func fetchImageData(of url: URL) async -> Data? { + do { + guard let imageURL = Bundle(for: type(of: self)).url(forResource: "sample", withExtension: "jpeg") else { + return nil + } + let mockData = try? Data(contentsOf: imageURL) + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + + let data = try await provider.request(url: url) + return data + } catch { + os_log(.error, log: .data, "%@", error.localizedDescription) + return nil + } + } +} diff --git a/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift b/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift new file mode 100644 index 0000000..4243fa9 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift @@ -0,0 +1,201 @@ +// +// HomeInteractorTests.swift +// Layover +// +// Created by 김인환 on 12/4/23. +// Copyright (c) 2023 CodeBomber. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +@testable import Layover +import XCTest + +final class HomeInteractorTests: XCTestCase { + + // MARK: Subject under test + typealias Models = HomeModels + var sut: HomeInteractor! + + // MARK: - Test lifecycle + + override func setUp() { + super.setUp() + setupHomeInteractor() + } + + // MARK: - Test setup + + func setupHomeInteractor() { + sut = HomeInteractor() + sut.homeWorker = MockHomeWorker() + } + + // MARK: - Test doubles + + final class HomePresentationLogicSpy: HomePresentationLogic { // 호출 테스트를 위한 Spy + var presentPostsCalled = false + var presentPostsReceivedResponse: Models.FetchPosts.Response! + var presentThumbnailImageCalled = false + var presentThumbnailImageReceivedResponse: Models.FetchThumbnailImageData.Response! + var presentPlaybackSceneCalled = false + var presentTagPlayListCalled = false + + func presentPosts(with response: Models.FetchPosts.Response) { + presentPostsCalled = true + presentPostsReceivedResponse = response + } + + func presentThumbnailImage(with response: Models.FetchThumbnailImageData.Response) { + presentThumbnailImageCalled = true + presentThumbnailImageReceivedResponse = response + } + + func presentPlaybackScene(with response: Models.PlayPosts.Response) { + presentPlaybackSceneCalled = true + } + + func presentTagPlayList(with response: Models.ShowTagPlayList.Response) { + presentTagPlayListCalled = true + } + } + + // MARK: - Tests + + func test_fetchPost는_presenter의_presentPosts를_호출한다() async throws { + // Arrange + let spy = HomePresentationLogicSpy() + sut.presenter = spy + let request = Models.FetchPosts.Request() + + // Act + _ = await sut.fetchPosts(with: request).value + + // Assert + XCTAssertTrue(spy.presentPostsCalled, "fetchPost()가 presenter의 presentPosts()를 호출했다.") + } + + func test_fetchPost는_presenter에게_올바른_데이터를_전달한다() async throws { + // Arrange + let spy = HomePresentationLogicSpy() + sut.presenter = spy + let request = Models.FetchPosts.Request() + + // Act + _ = await sut.fetchPosts(with: request).value + + // Assert + XCTAssertEqual(spy.presentPostsReceivedResponse.posts.count, 4, "fetchPost()가 presenter에게 올바른 데이터를 저장했다.") + } + + func test_fetchThumbnailImageData는_presenter의_presentThumbnailImage를_호출한다() async throws { + // Arrange + let spy = HomePresentationLogicSpy() + sut.presenter = spy + + guard let imageURL = URL(string: "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize") else { + XCTFail("URL 생성 실패") + return + } + + let request = Models.FetchThumbnailImageData.Request(imageURL: imageURL, indexPath: IndexPath()) + + // Act + _ = await sut.fetchThumbnailImageData(with: request).value + + // Assert + XCTAssertTrue(spy.presentThumbnailImageCalled, "fetchThumbnailImageData()가 presenter의 presentThumbnailImage()를 호출했다.") + } + + func test_fetchThumbnailImageData는_presenter에게_올바른_데이터를_전달한다() async throws { + // Arrange + let spy = HomePresentationLogicSpy() + sut.presenter = spy + + guard let imageURL = URL(string: "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize") else { + XCTFail("URL 생성 실패") + return + } + + let request = Models.FetchThumbnailImageData.Request(imageURL: imageURL, indexPath: IndexPath()) + + // Act + _ = await sut.fetchThumbnailImageData(with: request).value + + // Assert + let assertImageData = try Data(contentsOf: Bundle(for: type(of: self)).url(forResource: "sample", withExtension: "jpeg")!) + XCTAssertEqual(spy.presentThumbnailImageReceivedResponse.imageData, assertImageData,"fetchThumbnailImageData()가 presenter에게 올바른 데이터를 저장했다.") + } + + func test_playPosts는_자신의_selectedIndex값을_변경한다() async throws { + // Arrange + let request = Models.PlayPosts.Request(selectedIndex: 101) + + // Act + sut.playPosts(with: request) + + // Assert + XCTAssertEqual(sut.postPlayStartIndex, 101, "playPosts()가 자신의 selectedIndex를 변경했다.") + } + + func test_playPosts는_presenter의_presentPlaybackScene를_호출한다() async throws { + // Arrange + let spy = HomePresentationLogicSpy() + sut.presenter = spy + let request = Models.PlayPosts.Request(selectedIndex: 0) + + // Act + sut.playPosts(with: request) + + // Assert + XCTAssertTrue(spy.presentPlaybackSceneCalled, "playPosts()가 presenter의 presentPlaybackScene()를 호출했다.") + } + + // TODO: - videoFileWorker Mock 객체 생성 후 테스트 코드 작성 +// func testSelectVideo는_자신의_selectedVideoURL값을_변경한다() { +// // Arrange +// guard let dummyVideoURL = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8") else { +// XCTFail("URL 생성 실패") +// return +// } +// let request = Models.SelectVideo.Request(videoURL: dummyVideoURL) +// +// // Act +// sut.selectVideo(with: request) +// +// // Assert +// +// guard let selectedVideoURL = sut.selectedVideoURL else { +// XCTFail("selectedVideoURL이 nil") +// return +// } +// +// XCTAssertEqual(selectedVideoURL, dummyVideoURL) +// } + + func test_showTagPlayList는_자신의_selectedTag값을_변경한다() { + // Arrange + let request = Models.ShowTagPlayList.Request(tag: "DummyTag") + + // Act + sut.showTagPlayList(with: request) + + // Assert + XCTAssertEqual(sut.selectedTag, "DummyTag", "showTagPlayList()가 자신의 selectedTag를 변경했다.") + } + + func test_showTagPlayList는_presenter의_presentTagPlayList를_호출한다() { + // Arrange + let spy = HomePresentationLogicSpy() + sut.presenter = spy + let request = Models.ShowTagPlayList.Request(tag: "DummyTag") + + // Act + sut.showTagPlayList(with: request) + + // Assert + XCTAssertTrue(spy.presentTagPlayListCalled, "showTagPlayList()가 presenter의 presentTagPlayList()를 호출했다.") + } +} diff --git a/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift b/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift new file mode 100644 index 0000000..a2ce4a7 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift @@ -0,0 +1,213 @@ +// +// HomePresenterTests.swift +// Layover +// +// Created by 김인환 on 12/4/23. +// Copyright (c) 2023 CodeBomber. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +@testable import Layover +import XCTest + +final class HomePresenterTests: XCTestCase { + // MARK: - Subject under test + var sut: HomePresenter! + + typealias Models = HomeModels + + // MARK: - Test lifecycle + + override func setUp() { + super.setUp() + setupHomePresenter() + } + + override func tearDown() { + super.tearDown() + } + + // MARK: - Test setup + + func setupHomePresenter() { + sut = HomePresenter() + } + + // MARK: - Test doubles + + final class HomeDisplayLogicSpy: HomeDisplayLogic { + var displayPostsCalled = false + var displayPostsReceivedViewModel: Models.FetchPosts.ViewModel! + var displayThumbnailImageCalled = false + var displayThumbnailImageReceivedViewModel: Models.FetchThumbnailImageData.ViewModel! + var routeToPlaybackCalled = false + var routeToTagPlayListCalled = false + + func displayPosts(with viewModel: Layover.HomeModels.FetchPosts.ViewModel) { + displayPostsCalled = true + displayPostsReceivedViewModel = viewModel + } + + func displayThumbnailImage(with viewModel: Layover.HomeModels.FetchThumbnailImageData.ViewModel) { + displayThumbnailImageCalled = true + displayThumbnailImageReceivedViewModel = viewModel + } + + func routeToPlayback() { + routeToPlaybackCalled = true + } + + func routeToTagPlayList() { + routeToTagPlayListCalled = true + } + } + + // MARK: - Tests + + func test_presentPosts는_데이터를_받아오면_뷰의_displayPosts를_실행한다() { + // arrange + let spy = HomeDisplayLogicSpy() + sut.viewController = spy + + guard let imageURL = URL(string: "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize") else { + XCTFail("이미지 URL 생성 오류.") + return + } + + guard let dummyVideoURL = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8") else { + XCTFail("비디오 URL 생성 실패") + return + } + + let post = Post(member: Member(identifier: 1, + username: "안유진", + introduce: "난 아이브의 리더~", + profileImageURL: imageURL), + board: Board(identifier: 1, + title: "IZONE", + description: "아이즈원", + thumbnailImageURL: imageURL, + videoURL: dummyVideoURL, + latitude: 0.1203931, + longitude: 0.1029382), + tag: ["유진", "아이브", "IVE",]) + let response = Models.FetchPosts.Response(posts: [post]) + + // act + sut.presentPosts(with: response) + + // assert + XCTAssertTrue(spy.displayPostsCalled, "presentPosts는 displayPosts를 실행해서 뷰에게 데이터를 전달했다.") + } + + func test_presentPosts는_데이터의_썸네일_이미지_URL이_nil인_경우_뷰에게_해당_데이터를_전달하지_않는다() { + // arrange + let spy = HomeDisplayLogicSpy() + sut.viewController = spy + + guard let imageURL = URL(string: "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize") else { + XCTFail("이미지 URL 생성 오류.") + return + } + + guard let dummyVideoURL = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8") else { + XCTFail("비디오 URL 생성 실패") + return + } + + let post = Post(member: Member(identifier: 1, + username: "안유진", + introduce: "난 아이브의 리더~", + profileImageURL: imageURL), + board: Board(identifier: 1, + title: "IZONE", + description: "아이즈원", + thumbnailImageURL: nil, // nil + videoURL: dummyVideoURL, + latitude: 0.1203931, + longitude: 0.1029382), + tag: ["유진", "아이브", "IVE",]) + let response = Models.FetchPosts.Response(posts: [post]) + + // act + sut.presentPosts(with: response) + + // assert + XCTAssertTrue(spy.displayPostsCalled, "presentPosts는 displayPosts를 실행했다.") + XCTAssertEqual(spy.displayPostsReceivedViewModel.displayedPosts.count, 0, "썸네일 이미지 URL이 nil인 데이터는 뷰에게 전달하지 않는다.") + } + + func test_presentPosts는_데이터의_비디오_URL이_nil인_경우_뷰에게_해당_데이터를_전달하지_않는다() { + // arrange + let spy = HomeDisplayLogicSpy() + sut.viewController = spy + + guard let imageURL = URL(string: "https://cdnimg.melon.co.kr/resource/image/cds/musicstory/imgUrl20210831030133473.jpg/melon/quality/90/optimize") else { + XCTFail("이미지 URL 생성 오류.") + return + } + + let post = Post(member: Member(identifier: 1, + username: "안유진", + introduce: "난 아이브의 리더~", + profileImageURL: imageURL), + board: Board(identifier: 1, + title: "IZONE", + description: "아이즈원", + thumbnailImageURL: imageURL, + videoURL: nil, // nil + latitude: 0.1203931, + longitude: 0.1029382), + tag: ["유진", "아이브", "IVE",]) + let response = Models.FetchPosts.Response(posts: [post]) + + // act + sut.presentPosts(with: response) + + // assert + XCTAssertTrue(spy.displayPostsCalled, "presentPosts는 displayPosts를 실행했다.") + XCTAssertEqual(spy.displayPostsReceivedViewModel.displayedPosts.count, 0, "비디오 URL이 nil인 데이터는 뷰에게 전달하지 않는다.") + } + + func test_presentThumbnailImage는_데이터를_받아오면_뷰의_displayThumbnailImage를_실행한다() { + // arrange + let spy = HomeDisplayLogicSpy() + sut.viewController = spy + + let response = Models.FetchThumbnailImageData.Response(imageData: Data(), indexPath: IndexPath()) + + // act + sut.presentThumbnailImage(with: response) + + // assert + XCTAssertTrue(spy.displayThumbnailImageCalled, "presentThumbnailImage는 displayThumbnailImage를 실행해서 뷰에게 데이터를 전달했다.") + } + + func test_presentPlaybackScene은_뷰의_routeToPlayback을_실행한다() { + // arrange + let spy = HomeDisplayLogicSpy() + sut.viewController = spy + + // act + sut.presentPlaybackScene(with: Models.PlayPosts.Response()) + + // assert + XCTAssertTrue(spy.routeToPlaybackCalled, "presentPlaybackScene은 routeToPlayback을 실행했다.") + } + + func test_presentTagPlayList는_뷰의_routeToTagPlayList를_실행한다() { + // arrange + let spy = HomeDisplayLogicSpy() + sut.viewController = spy + + // act + sut.presentTagPlayList(with: Models.ShowTagPlayList.Response()) + + // assert + XCTAssertTrue(spy.routeToTagPlayListCalled, "presentTagPlayListScene은 routeToTagPlayList를 실행했다.") + } + +} diff --git a/iOS/Layover/LayoverTests/Scenes/Home/HomeViewControllerTests.swift b/iOS/Layover/LayoverTests/Scenes/Home/HomeViewControllerTests.swift new file mode 100644 index 0000000..9fc459c --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/Home/HomeViewControllerTests.swift @@ -0,0 +1,122 @@ +// +// HomeViewControllerTests.swift +// Layover +// +// Created by 김인환 on 12/4/23. +// Copyright (c) 2023 CodeBomber. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +@testable import Layover +import XCTest + +// View는 액션이 섞여있기에 가능한 정도 까지만 테스트 작성 +final class HomeViewControllerTests: XCTestCase { + // MARK: Subject under test + + var sut: HomeViewController! + var window: UIWindow! + + // MARK: - Test lifecycle + + override func setUp() { + super.setUp() + window = UIWindow() + setupHomeViewController() + } + + override func tearDown() { + window = nil + super.tearDown() + } + + // MARK: - Test setup + + func setupHomeViewController() { + sut = HomeViewController() + } + + // MARK: - Test doubles + +// final class HomeBusinessLogicSpy: HomeBusinessLogic { +// var fetchPostsCalled = false +// var fetchThumbnailImageDataCalled = false +// var playPostsCalled = false +// var selectVideoCalled = false +// var showTagPlayListCalled = false +// +// func fetchPosts(with request: Layover.HomeModels.FetchPosts.Request) -> Task { +// fetchPostsCalled = true +// return Task { return true } +// } +// +// func fetchThumbnailImageData(with request: Layover.HomeModels.FetchThumbnailImageData.Request) -> Task { +// fetchThumbnailImageDataCalled = true +// return Task { return true } +// } +// +// func playPosts(with request: Layover.HomeModels.PlayPosts.Request) { +// playPostsCalled = true +// } +// +// func selectVideo(with request: Layover.HomeModels.SelectVideo.Request) { +// selectVideoCalled = true +// } +// +// func showTagPlayList(with request: Layover.HomeModels.ShowTagPlayList.Request) { +// showTagPlayListCalled = true +// } +// } + + final class HomeRouterSpy: HomeRouter { + var routeToNextCalled = false + var routeToEditVideoCalled = false + var routeToPlaybackCalled = false + var routeToTagPlayCalled = false + + override func routeToNext() { + routeToNextCalled = true + } + + override func routeToEditVideo() { + routeToEditVideoCalled = true + } + + override func routeToPlayback() { + routeToPlaybackCalled = true + } + + override func routeToTagPlay() { + routeToTagPlayCalled = true + } + } + + // MARK: - Tests + + func test_routeToPlayback_호출시_router의_routeToPlayback을_호출한다() { + // arrange + let spy = HomeRouterSpy() + sut.router = spy + + // act + sut.routeToPlayback() + + // assert + XCTAssertTrue(spy.routeToPlaybackCalled, "routeToPlayback 호출 시 router의 routeToPlayback이 호출되었다.") + } + + func test_routeToTagPlayList_호출시_router의_routeToTagPlay를_호출한다() { + // arrange + let spy = HomeRouterSpy() + sut.router = spy + + // act + sut.routeToTagPlayList() + + // assert + XCTAssertTrue(spy.routeToTagPlayCalled, "routeToTagPlayList 호출 시 router의 routeToTagPlay가 호출되었다.") + } +} diff --git a/iOS/Layover/LayoverTests/Scenes/Home/HomeWorkerTests.swift b/iOS/Layover/LayoverTests/Scenes/Home/HomeWorkerTests.swift new file mode 100644 index 0000000..6620b82 --- /dev/null +++ b/iOS/Layover/LayoverTests/Scenes/Home/HomeWorkerTests.swift @@ -0,0 +1,82 @@ +// +// HomeWorkerTests.swift +// Layover +// +// Created by 김인환 on 12/4/23. +// Copyright (c) 2023 CodeBomber. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +@testable import Layover +import XCTest + +final class HomeWorkerTests: XCTestCase { + // MARK: Subject under test + + var sut: HomeWorker! + + // MARK: - Test lifecycle + + override func setUp() { + super.setUp() + setupHomeWorker() + } + + override func tearDown() { + super.tearDown() + } + + // MARK: - Test setup + + func setupHomeWorker() { + sut = HomeWorker(provider: Provider(session: .initMockSession(), authManager: StubAuthManager())) + } + + // MARK: - Tests + + func test_fetchPost는_성공적으로_데이터를_받아오면_Post배열을_리턴한다() async throws { + // arrange + guard let mockFileLocation = Bundle(for: type(of: self)).url(forResource: "PostList", 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) + } + var result: [Post]? + + // act + result = await sut.fetchPosts() + + // assert + XCTAssertNotNil(result) + XCTAssertEqual(result?.count, 4) + } + + func test_fetchPost는_데이터를_받아오지_못하면_nil을_리턴한다() async { + // arrange + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 404, + httpVersion: nil, + headerFields: nil) + return (response, nil, nil) + } + var result: [Post]? + + // act + result = await sut.fetchPosts() + + // assert + XCTAssertNil(result) + } +}