Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ jobs:
schemes: "App"
- name: Domain-Data
schemes: "Domain Data"
- name: Infra
schemes: "Infra"
- name: Persistence
schemes: "Persistence"
- name: Presentation
Expand Down
2 changes: 2 additions & 0 deletions Application/App/Sources/Resource/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<dict>
<key>APP_STORE_URL</key>
<string>$(APP_STORE_URL)</string>
<key>APP_STORE_LOOKUP_URL</key>
<string>$(APP_STORE_LOOKUP_URL)</string>
<key>TESTFLIGHT_URL</key>
<string>$(TESTFLIGHT_URL)</string>
<key>CFBundleDevelopmentRegion</key>
Expand Down
2 changes: 1 addition & 1 deletion Application/Data/Sources/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public final class DataAssembler: Assembler {

container.register(AppVersionRepository.self) {
AppVersionRepositoryImpl(
service: container.resolve(AppVersionConfigurationService.self)
service: container.resolve(AppStoreVersionService.self)
)
}

Expand Down
10 changes: 10 additions & 0 deletions Application/Data/Sources/Protocol/AppStoreVersionService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// AppStoreVersionService.swift
// Data
//
// Created by opfic on 7/22/26.
//

public protocol AppStoreVersionService {
func fetchLatestVersion() async throws -> String
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import Domain

final class AppVersionRepositoryImpl: AppVersionRepository {
private let service: AppVersionConfigurationService
private let service: AppStoreVersionService

init(service: AppVersionConfigurationService) {
init(service: AppStoreVersionService) {
self.service = service
}

func fetchRequiredVersion() async throws -> String {
try await service.fetchRequiredVersion()
func fetchLatestVersion() async throws -> String {
try await service.fetchLatestVersion()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,37 @@ import Testing
@testable import Data

struct AppVersionRepositoryImplTests {
@Test("필수 버전 조회는 구성 서비스의 값을 반환한다")
func 필수_버전_조회는_구성_서비스의_값을_반환한다() async throws {
let service = AppVersionConfigurationServiceSpy(result: .success("1.5"))
@Test("최신 버전 조회는 App Store 서비스의 값을 반환한다")
func 최신_버전_조회는_App_Store_서비스의_값을_반환한다() async throws {
let service = AppStoreVersionServiceSpy(result: .success("1.5"))
let repository = AppVersionRepositoryImpl(service: service)

#expect(try await repository.fetchRequiredVersion() == "1.5")
#expect(try await repository.fetchLatestVersion() == "1.5")
#expect(await service.fetchCallCount() == 1)
}

@Test("필수 버전 조회는 구성 서비스의 오류를 그대로 반환한다")
func 필수_버전_조회는_구성_서비스의_오류를_그대로_반환한다() async {
let service = AppVersionConfigurationServiceSpy(
result: .failure(AppVersionConfigurationServiceTestError.fetchFailed)
@Test("최신 버전 조회는 App Store 서비스의 오류를 그대로 반환한다")
func 최신_버전_조회는_App_Store_서비스의_오류를_그대로_반환한다() async {
let service = AppStoreVersionServiceSpy(
result: .failure(AppStoreVersionServiceTestError.fetchFailed)
)
let repository = AppVersionRepositoryImpl(service: service)

await #expect(throws: AppVersionConfigurationServiceTestError.fetchFailed) {
try await repository.fetchRequiredVersion()
await #expect(throws: AppStoreVersionServiceTestError.fetchFailed) {
try await repository.fetchLatestVersion()
}
}
}

private actor AppVersionConfigurationServiceSpy: AppVersionConfigurationService {
private actor AppStoreVersionServiceSpy: AppStoreVersionService {
private let result: Result<String, Error>
private var count = 0

init(result: Result<String, Error>) {
self.result = result
}

func fetchRequiredVersion() async throws -> String {
func fetchLatestVersion() async throws -> String {
count += 1
return try result.get()
}
Expand All @@ -49,6 +49,6 @@ private actor AppVersionConfigurationServiceSpy: AppVersionConfigurationService
}
}

private enum AppVersionConfigurationServiceTestError: Error {
private enum AppStoreVersionServiceTestError: Error {
case fetchFailed
}
8 changes: 2 additions & 6 deletions Application/Domain/Sources/Entity/AppVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@
public struct AppVersion: Comparable {
private let components: [Int]

public init(marketingVersion: String, buildNumber: String) throws {
try self.init("\(marketingVersion).\(buildNumber)")
}

init(_ value: String) throws {
let rawComponents = value.split(separator: ".", omittingEmptySubsequences: false)
public init(_ marketingVersion: String) throws {
let rawComponents = marketingVersion.split(separator: ".", omittingEmptySubsequences: false)
guard !rawComponents.isEmpty,
rawComponents.allSatisfy({ !$0.isEmpty && $0.allSatisfy(\.isNumber) }) else {
throw DomainLayerError.invalidData(context: "appVersion")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
//

public protocol AppVersionRepository {
func fetchRequiredVersion() async throws -> String
func fetchLatestVersion() async throws -> String
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,19 @@ public final class CheckAppUpdateUseCaseImpl: CheckAppUpdateUseCase {
}

public func execute() async throws -> Bool {
let requiredVersionValue = try await repository.fetchRequiredVersion()
let requiredVersion = try AppVersion(requiredVersionValue)
let latestVersionValue = try await repository.fetchLatestVersion()
let latestVersion = try AppVersion(latestVersionValue)
let currentVersion = try currentVersion()
return currentVersion < requiredVersion
return currentVersion < latestVersion
}

private func currentVersion() throws -> AppVersion {
guard let marketingVersion = Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString"
) as? String,
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else {
) as? String else {
throw DomainLayerError.invalidData(context: "appVersion")
}

return try AppVersion(
marketingVersion: marketingVersion,
buildNumber: buildNumber
)
return try AppVersion(marketingVersion)
}
}
39 changes: 33 additions & 6 deletions Application/Domain/Tests/Entity/AppVersionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,41 @@ struct AppVersionTests {
@Test("마케팅 버전 형식이 잘못되면 invalidData 오류를 반환한다")
func 마케팅_버전_형식이_잘못되면_invalidData_오류를_반환한다() {
#expect(throws: DomainLayerError.self) {
try AppVersion(marketingVersion: "1..5", buildNumber: "127")
try AppVersion("1..5")
}
}

@Test("빌드 번호 형식이 잘못되면 invalidData 오류를 반환한다")
func 빌드_번호_형식이_잘못되면_invalidData_오류를_반환한다() {
#expect(throws: DomainLayerError.self) {
try AppVersion(marketingVersion: "1.5", buildNumber: "127a")
}
@Test("낮은 마케팅 버전은 높은 마케팅 버전보다 작다")
func 낮은_마케팅_버전은_높은_마케팅_버전보다_작다() throws {
let lowerVersion = try AppVersion("1.4.9")
let higherVersion = try AppVersion("1.5.0")

#expect(lowerVersion < higherVersion)
}

@Test("같은 마케팅 버전은 서로 작지 않다")
func 같은_마케팅_버전은_서로_작지_않다() throws {
let version = try AppVersion("1.5.0")
let sameVersion = try AppVersion("1.5.0")

#expect(!(version < sameVersion))
#expect(!(sameVersion < version))
}

@Test("높은 마케팅 버전은 낮은 마케팅 버전보다 작지 않다")
func 높은_마케팅_버전은_낮은_마케팅_버전보다_작지_않다() throws {
let higherVersion = try AppVersion("1.5.1")
let lowerVersion = try AppVersion("1.5.0")

#expect(!(higherVersion < lowerVersion))
}

@Test("생략된 마케팅 버전 구성 요소는 0으로 비교한다")
func 생략된_마케팅_버전_구성_요소는_0으로_비교한다() throws {
let version = try AppVersion("1.5")
let expandedVersion = try AppVersion("1.5.0")

#expect(!(version < expandedVersion))
#expect(!(expandedVersion < version))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,36 @@ import Testing
@testable import Domain

struct CheckAppUpdateUseCaseImplTests {
@Test("Bundle의 현재 버전보다 필수 버전이 높으면 업데이트가 필요하다")
func Bundle의_현재_버전보다_필수_버전이_높으면_업데이트가_필요하다() async throws {
let requiredVersion = "\(try currentVersionValue()).1"
let repository = AppVersionRepositorySpy(result: .success(requiredVersion))
@Test("Bundle의 현재 버전보다 최신 버전이 높으면 업데이트가 필요하다")
func Bundle의_현재_버전보다_최신_버전이_높으면_업데이트가_필요하다() async throws {
let latestVersion = "\(try currentVersionValue()).1"
let repository = AppVersionRepositorySpy(result: .success(latestVersion))
let useCase = CheckAppUpdateUseCaseImpl(repository)

#expect(try await useCase.execute())
#expect(await repository.fetchCallCount() == 1)
}

@Test("Bundle의 현재 버전과 필수 버전이 같으면 업데이트가 필요하지 않다")
func Bundle의_현재_버전과_필수_버전이_같으면_업데이트가_필요하지_않다() async throws {
@Test("Bundle의 현재 버전과 최신 버전이 같으면 업데이트가 필요하지 않다")
func Bundle의_현재_버전과_최신_버전이_같으면_업데이트가_필요하지_않다() async throws {
let repository = AppVersionRepositorySpy(result: .success(try currentVersionValue()))
let useCase = CheckAppUpdateUseCaseImpl(repository)

#expect(try await !useCase.execute())
#expect(await repository.fetchCallCount() == 1)
}

@Test("필수 버전 형식이 잘못되면 invalidData 오류를 반환한다")
func 필수_버전_형식이_잘못되면_invalidData_오류를_반환한다() async throws {
@Test("최신 버전 형식이 잘못되면 invalidData 오류를 반환한다")
func 최신_버전_형식이_잘못되면_invalidData_오류를_반환한다() async throws {
let repository = AppVersionRepositorySpy(result: .success("latest"))
let useCase = CheckAppUpdateUseCaseImpl(repository)
await #expect(throws: DomainLayerError.self) {
try await useCase.execute()
}
}

@Test("필수 버전 조회 오류를 그대로 반환한다")
func 필수_버전_조회_오류를_그대로_반환한다() async throws {
@Test("최신 버전 조회 오류를 그대로 반환한다")
func 최신_버전_조회_오류를_그대로_반환한다() async throws {
let repository = AppVersionRepositorySpy(
result: .failure(AppVersionRepositoryTestError.fetchFailed)
)
Expand All @@ -51,13 +51,9 @@ struct CheckAppUpdateUseCaseImplTests {
}

private func currentVersionValue() throws -> String {
let marketingVersion = try #require(
try #require(
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
)
let buildNumber = try #require(
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
)
return "\(marketingVersion).\(buildNumber)"
}

private actor AppVersionRepositorySpy: AppVersionRepository {
Expand All @@ -68,7 +64,7 @@ private actor AppVersionRepositorySpy: AppVersionRepository {
self.result = result
}

func fetchRequiredVersion() async throws -> String {
func fetchLatestVersion() async throws -> String {
count += 1
return try result.get()
}
Expand Down
7 changes: 7 additions & 0 deletions Application/Infra/Sources/Common/InfraLayerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ enum SocialLoginError: Error {
case authenticationAlreadyInProgress
}

enum AppVersionServiceError: Error, Equatable {
case missingLookupURL
case invalidLookupURL
case missingVersion
case invalidVersion
}

extension Error {
var isSocialLoginCancelled: Bool {
switch self {
Expand Down
4 changes: 2 additions & 2 deletions Application/Infra/Sources/InfraAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public final class InfraAssembler: Assembler {
FirebaseAppServiceImpl()
}

container.register(AppVersionConfigurationService.self) {
RemoteConfigAppVersionServiceImpl()
container.register(AppStoreVersionService.self) {
ITunesAppVersionServiceImpl()
}

container.register(AnalyticsService.self) {
Expand Down
Loading
Loading