-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAccountFeatureTests.swift
More file actions
408 lines (338 loc) · 12.6 KB
/
Copy pathAccountFeatureTests.swift
File metadata and controls
408 lines (338 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//
// AccountFeatureTests.swift
// DevLogPresentationTests
//
// Created by opfic on 6/11/26.
//
// swiftlint:disable file_length
import Testing
import ComposableArchitecture
import Foundation
import DevLogDomain
@testable import DevLogPresentation
@MainActor
struct AccountFeatureTests {
@Test("화면이 나타나면 인증 제공자 목록을 가져와 상태에 반영한다")
func 화면이_나타나면_인증_제공자_목록을_가져와_상태에_반영한다() async {
let fetchSpy = FetchAuthProvidersUseCaseSpy(
currentProvider: .google,
allProviders: [.google, .github]
)
let driver = AccountTestDriver(fetchUseCase: fetchSpy)
driver.onAppear()
await waitUntil {
driver.currentProvider == .google
}
#expect(fetchSpy.executeCallCount == 1)
#expect(driver.connectedProviders == [.github])
#expect(driver.disconnectedProviders == [.apple])
}
@Test("연동에 성공하면 선택한 제공자를 연동하고 제공자 목록을 다시 가져온다")
func 연동에_성공하면_선택한_제공자를_연동하고_제공자_목록을_다시_가져온다() async {
let fetchSpy = FetchAuthProvidersUseCaseSpy(
currentProvider: .google,
allProviders: [.google, .github]
)
let linkSpy = LinkAuthProviderUseCaseSpy()
let driver = AccountTestDriver(
fetchUseCase: fetchSpy,
linkUseCase: linkSpy
)
driver.linkWithProvider(.github)
await waitUntil {
linkSpy.providers == [.github] && fetchSpy.executeCallCount == 1
}
#expect(driver.currentProvider == .google)
#expect(driver.connectedProviders == [.github])
#expect(driver.disconnectedProviders == [.apple])
}
@Test("연동 해제에 성공하면 선택한 제공자를 해제하고 제공자 목록을 다시 가져온다")
func 연동_해제에_성공하면_선택한_제공자를_해제하고_제공자_목록을_다시_가져온다() async {
let fetchSpy = FetchAuthProvidersUseCaseSpy(
currentProvider: .google,
allProviders: [.google]
)
let unlinkSpy = UnlinkAuthProviderUseCaseSpy()
let driver = AccountTestDriver(
fetchUseCase: fetchSpy,
unlinkUseCase: unlinkSpy
)
driver.unlinkFromProvider(.github)
await waitUntil {
unlinkSpy.providers == [.github] && fetchSpy.executeCallCount == 1
}
#expect(driver.currentProvider == .google)
#expect(driver.connectedProviders.isEmpty)
#expect(driver.disconnectedProviders == [.apple, .github])
}
@Test("연동 작업이 지연되면 로딩 상태를 표시하고 완료되면 해제한다")
func 연동_작업이_지연되면_로딩_상태를_표시하고_완료되면_해제한다() async {
let clock = TestClock()
let fetchSpy = FetchAuthProvidersUseCaseSpy(
currentProvider: .google,
allProviders: [.google, .github]
)
let linkSpy = LinkAuthProviderUseCaseSpy()
linkSpy.shouldSuspend = true
let target = LoadingFeature.Target.default
let store = TestStore(initialState: AccountFeature.State()) {
AccountFeature()
} withDependencies: {
$0.fetchAuthProvidersUseCase = fetchSpy
$0.linkAuthProviderUseCase = linkSpy
$0.unlinkAuthProviderUseCase = UnlinkAuthProviderUseCaseSpy()
$0.continuousClock = clock
}
await store.send(.linkWithProvider(.github)) {
$0.activeLoadingProvider = .github
}
await store.receive(\.loading.begin) {
$0.loading.delayedCountByTarget[target] = 1
$0.loading.scheduledDelayedTargets = [target]
}
#expect(linkSpy.providers == [.github])
#expect(!store.state.isLoading)
await clock.advance(by: .milliseconds(300))
await store.receive(\.loading.delayedLoadingDidBecomeVisible, target) {
$0.loading.scheduledDelayedTargets = []
$0.loading.visibleDelayedTargets = [target]
$0.loading.visibleTargets = [target]
$0.loading.isLoading = true
}
#expect(store.state.isLoading)
#expect(store.state.activeLoadingProvider == .github)
linkSpy.resume()
await store.receive(\.setProviders) {
$0.currentProvider = .google
$0.connectedProviders = [.github]
$0.disconnectedProviders = [.apple]
}
await store.receive(\.loading.end) {
$0.loading.delayedCountByTarget[target] = 0
$0.loading.visibleDelayedTargets = []
$0.loading.visibleTargets = []
$0.loading.isLoading = false
$0.activeLoadingProvider = nil
}
#expect(!store.state.isLoading)
}
@Test("인증 제공자 조회에 실패하면 공통 에러 알림을 표시한다")
func 인증_제공자_조회에_실패하면_공통_에러_알림을_표시한다() async {
let fetchSpy = FetchAuthProvidersUseCaseSpy()
fetchSpy.error = AccountTestError.failure
let driver = AccountTestDriver(fetchUseCase: fetchSpy)
driver.onAppear()
await waitUntil {
driver.alert != nil
}
#expect(driver.alert == expectedAlert(
title: String(localized: "common_error_title"),
message: String(localized: "common_error_message")
))
}
@Test("연동 실패 에러 유형에 맞는 알림을 표시한다")
func 연동_실패_에러_유형에_맞는_알림을_표시한다() async {
let scenarios = [
AccountLinkFailureScenario(
error: AuthError.linkEmailNotFound,
title: String(localized: "account_alert_email_unavailable_title"),
message: String(localized: "account_alert_email_unavailable_message")
),
AccountLinkFailureScenario(
error: AuthError.linkEmailMismatch,
title: String(localized: "account_alert_cannot_link_title"),
message: String(localized: "account_alert_cannot_link_message")
),
AccountLinkFailureScenario(
error: AuthError.linkCredentialAlreadyInUse,
title: String(localized: "account_alert_already_linked_title"),
message: String(localized: "account_alert_already_linked_message")
),
AccountLinkFailureScenario(
error: AccountTestError.failure,
title: String(localized: "common_error_title"),
message: String(localized: "common_error_message")
)
]
for scenario in scenarios {
let linkSpy = LinkAuthProviderUseCaseSpy()
linkSpy.error = scenario.error
let driver = AccountTestDriver(linkUseCase: linkSpy)
driver.linkWithProvider(.github)
await waitUntil {
driver.alert != nil
}
#expect(driver.alert == expectedAlert(
title: scenario.title,
message: scenario.message
))
}
}
@Test("소셜 로그인이 취소되어도 연동 알림을 표시하지 않는다")
func 소셜_로그인이_취소되어도_연동_알림을_표시하지_않는다() async {
let linkSpy = LinkAuthProviderUseCaseSpy()
linkSpy.linked = false
let driver = AccountTestDriver(linkUseCase: linkSpy)
driver.linkWithProvider(.google)
await waitUntil {
linkSpy.providers == [.google] && !driver.isLoading
}
#expect(driver.alert == nil)
}
@Test("연동 해제 실패 시 공통 에러 알림을 표시한다")
func 연동_해제_실패_시_공통_에러_알림을_표시한다() async {
let unlinkSpy = UnlinkAuthProviderUseCaseSpy()
unlinkSpy.error = AccountTestError.failure
let driver = AccountTestDriver(unlinkUseCase: unlinkSpy)
driver.unlinkFromProvider(.github)
await waitUntil {
driver.alert != nil
}
#expect(driver.alert == expectedAlert(
title: String(localized: "common_error_title"),
message: String(localized: "common_error_message")
))
}
@Test("알림을 닫으면 알림 상태가 초기화된다")
func 알림을_닫으면_알림_상태가_초기화된다() async {
let fetchSpy = FetchAuthProvidersUseCaseSpy()
fetchSpy.error = AccountTestError.failure
let driver = AccountTestDriver(fetchUseCase: fetchSpy)
driver.onAppear()
await waitUntil {
driver.alert != nil
}
driver.dismissAlert()
#expect(driver.alert == nil)
}
}
@MainActor
private struct AccountTestDriver {
private let feature: StoreOf<AccountFeature>
var currentProvider: AuthProvider? {
feature.state.currentProvider
}
var connectedProviders: [AuthProvider] {
feature.state.connectedProviders
}
var disconnectedProviders: [AuthProvider] {
feature.state.disconnectedProviders
}
var isLoading: Bool {
feature.state.isLoading
}
var alert: AlertState<Never>? {
feature.state.alert
}
init(
fetchUseCase: FetchAuthProvidersUseCase = FetchAuthProvidersUseCaseSpy(),
linkUseCase: LinkAuthProviderUseCase = LinkAuthProviderUseCaseSpy(),
unlinkUseCase: UnlinkAuthProviderUseCase = UnlinkAuthProviderUseCaseSpy()
) {
feature = Store(initialState: AccountFeature.State()) {
AccountFeature()
} withDependencies: {
$0.fetchAuthProvidersUseCase = fetchUseCase
$0.linkAuthProviderUseCase = linkUseCase
$0.unlinkAuthProviderUseCase = unlinkUseCase
$0.continuousClock = ContinuousClock()
}
}
func onAppear() {
feature.send(.onAppear)
}
func linkWithProvider(_ provider: AuthProvider) {
feature.send(.linkWithProvider(provider))
}
func unlinkFromProvider(_ provider: AuthProvider) {
feature.send(.unlinkFromProvider(provider))
}
func dismissAlert() {
feature.send(.alert(.dismiss))
}
}
private struct AccountLinkFailureScenario {
let error: Error
let title: String
let message: String
}
private final class FetchAuthProvidersUseCaseSpy: FetchAuthProvidersUseCase {
var currentProvider: AuthProvider?
var allProviders: [AuthProvider]
var error: Error?
private(set) var executeCallCount = 0
init(
currentProvider: AuthProvider? = nil,
allProviders: [AuthProvider] = []
) {
self.currentProvider = currentProvider
self.allProviders = allProviders
}
func execute() async throws -> (currentProvider: AuthProvider?, allProviders: [AuthProvider]) {
executeCallCount += 1
if let error {
throw error
}
return (currentProvider, allProviders)
}
}
private final class LinkAuthProviderUseCaseSpy: LinkAuthProviderUseCase {
var error: Error?
var linked = true
var shouldSuspend = false
private(set) var providers = [AuthProvider]()
private var continuation: CheckedContinuation<Void, Never>?
private var shouldResume = false
func execute(_ provider: AuthProvider) async throws -> Bool {
providers.append(provider)
if shouldSuspend {
await withCheckedContinuation { continuation in
if shouldResume {
shouldResume = false
continuation.resume()
} else {
self.continuation = continuation
}
}
}
if let error {
throw error
}
return linked
}
func resume() {
guard let continuation else {
shouldResume = true
return
}
self.continuation = nil
continuation.resume()
}
}
private final class UnlinkAuthProviderUseCaseSpy: UnlinkAuthProviderUseCase {
var error: Error?
private(set) var providers = [AuthProvider]()
func execute(_ provider: AuthProvider) async throws {
providers.append(provider)
if let error {
throw error
}
}
}
private enum AccountTestError: Error {
case failure
}
private func expectedAlert(
title: String,
message: String
) -> AlertState<Never> {
AlertState {
TextState(title)
} actions: {
ButtonState(role: .cancel) {
TextState(String(localized: "common_close"))
}
} message: {
TextState(message)
}
}