Skip to content

Commit 0c53973

Browse files
committed
test: 온보딩, kakaoLogin UITest 코드 작성
1 parent 7c85259 commit 0c53973

11 files changed

Lines changed: 569 additions & 76 deletions

File tree

Common/Sources/DataSources/Keychain/JWTTokenStorageable.swift

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by 강동영 on 1/13/26.
66
//
77

8+
import Foundation
89

910
public protocol JWTTokenStorageable: Sendable {
1011
func save(accessToken: String, refreshToken: String?) throws
@@ -13,10 +14,10 @@ public protocol JWTTokenStorageable: Sendable {
1314
func exists() throws -> Bool
1415
}
1516

16-
public final class JWTokenStorage: KeychainTokenStorage, JWTTokenStorageable {
17+
public final class JWTokenStorage: /*KeychainTokenStorage,*/ JWTTokenStorageable {
1718
private let service: String
1819

19-
public override init(service: String = HambugKeychainKey.serviceID) {
20+
public init(service: String = HambugKeychainKey.serviceID) {
2021
self.service = service
2122
}
2223

@@ -44,3 +45,130 @@ public final class JWTokenStorage: KeychainTokenStorage, JWTTokenStorageable {
4445
try delete(.refreshToken)
4546
}
4647
}
48+
49+
extension JWTokenStorage {
50+
51+
func set(_ value: String, for key: HambugKeychainKey) throws {
52+
let data = value.data(using: .utf8)!
53+
let query: [String: Any] = [
54+
kSecClass as String: kSecClassGenericPassword,
55+
kSecAttrService as String: service,
56+
kSecAttrAccount as String: key.toString,
57+
kSecValueData as String: data
58+
]
59+
60+
let status = SecItemAdd(query as CFDictionary, nil)
61+
62+
do {
63+
try handleError(status)
64+
} catch KeychainError.duplicateItem {
65+
// 중복 된 아이템이 있다면 업데이트 수행
66+
try updateExisting(data, for: key)
67+
}
68+
}
69+
70+
func updateExisting(_ data: Data, for key: HambugKeychainKey) throws {
71+
let updateQuery: [String: Any] = [
72+
kSecClass as String: kSecClassGenericPassword,
73+
kSecAttrService as String: service,
74+
kSecAttrAccount as String: key.toString
75+
]
76+
77+
let attributes: [String: Any] = [
78+
kSecValueData as String: data
79+
]
80+
81+
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary)
82+
83+
try handleError(updateStatus)
84+
}
85+
86+
func string(for key: HambugKeychainKey) throws -> String? {
87+
let query: [String: Any] = [
88+
kSecClass as String: kSecClassGenericPassword,
89+
kSecAttrService as String: service,
90+
kSecAttrAccount as String: key.toString,
91+
kSecReturnData as String: true,
92+
kSecMatchLimit as String: kSecMatchLimitOne
93+
]
94+
95+
var item: CFTypeRef?
96+
let status = SecItemCopyMatching(query as CFDictionary, &item)
97+
98+
try handleError(status)
99+
100+
guard let data = item as? Data,
101+
let string = String(data: data, encoding: .utf8)
102+
else {
103+
throw KeychainError.unexpectedPasswordData
104+
}
105+
return string
106+
}
107+
108+
func delete(_ key: HambugKeychainKey) throws {
109+
let query: [String: Any] = [
110+
kSecClass as String: kSecClassGenericPassword,
111+
kSecAttrService as String: service,
112+
kSecAttrAccount as String: key.toString
113+
]
114+
115+
let status = SecItemDelete(query as CFDictionary)
116+
117+
try handleError(status)
118+
}
119+
120+
func contains(_ key: HambugKeychainKey) throws -> Bool {
121+
let query: [String: Any] = [
122+
kSecClass as String: kSecClassGenericPassword,
123+
kSecAttrService as String: service,
124+
kSecAttrAccount as String: key.toString,
125+
]
126+
127+
return SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess
128+
}
129+
130+
func handleError(_ status: OSStatus) throws {
131+
switch status {
132+
case errSecSuccess:
133+
return
134+
case errSecDuplicateItem:
135+
throw KeychainError.duplicateItem
136+
case errSecItemNotFound:
137+
throw KeychainError.itemNotFound
138+
default:
139+
throw KeychainError.unexpected(status)
140+
}
141+
}
142+
}
143+
144+
//public final class JWTokenStorage: KeychainTokenStorage, JWTTokenStorageable {
145+
// private let service: String
146+
//
147+
// public override init(service: String = HambugKeychainKey.serviceID) {
148+
// self.service = service
149+
// }
150+
//
151+
// public func save(accessToken: String, refreshToken: String?) throws {
152+
// try set(accessToken, for: .accessToken)
153+
//
154+
// if let refresh = refreshToken {
155+
// try set(refresh, for: .refreshToken)
156+
// }
157+
// }
158+
//
159+
// public func load() -> (accessToken: String?, refreshToken: String?) {
160+
// let access = try? string(for: .accessToken)
161+
// let refresh = try? string(for: .refreshToken)
162+
//
163+
// return (access, refresh)
164+
// }
165+
//
166+
// public func exists() throws -> Bool {
167+
// try contains(.accessToken)
168+
// }
169+
//
170+
// public func clear() throws {
171+
// try delete(.accessToken)
172+
// try delete(.refreshToken)
173+
// }
174+
//}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "2620"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
</BuildAction>
10+
<TestAction
11+
buildConfiguration = "Debug"
12+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
13+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
14+
shouldUseLaunchSchemeArgsEnv = "YES"
15+
shouldAutocreateTestPlan = "YES">
16+
<Testables>
17+
<TestableReference
18+
skipped = "NO"
19+
parallelizable = "YES">
20+
<BuildableReference
21+
BuildableIdentifier = "primary"
22+
BlueprintIdentifier = "915BC5E92E3CB9B80062B78E"
23+
BuildableName = "HambugUITests.xctest"
24+
BlueprintName = "HambugUITests"
25+
ReferencedContainer = "container:Hambug.xcodeproj">
26+
</BuildableReference>
27+
</TestableReference>
28+
</Testables>
29+
</TestAction>
30+
<LaunchAction
31+
buildConfiguration = "Debug"
32+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
33+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
34+
launchStyle = "0"
35+
useCustomWorkingDirectory = "NO"
36+
ignoresPersistentStateOnLaunch = "NO"
37+
debugDocumentVersioning = "YES"
38+
debugServiceExtension = "internal"
39+
allowLocationSimulation = "YES">
40+
<BuildableProductRunnable
41+
runnableDebuggingMode = "0">
42+
<BuildableReference
43+
BuildableIdentifier = "primary"
44+
BlueprintIdentifier = "915BC5D12E3CB9B50062B78E"
45+
BuildableName = "Hambug.app"
46+
BlueprintName = "Hambug"
47+
ReferencedContainer = "container:Hambug.xcodeproj">
48+
</BuildableReference>
49+
</BuildableProductRunnable>
50+
</LaunchAction>
51+
<ProfileAction
52+
buildConfiguration = "Release"
53+
shouldUseLaunchSchemeArgsEnv = "YES"
54+
savedToolIdentifier = ""
55+
useCustomWorkingDirectory = "NO"
56+
debugDocumentVersioning = "YES">
57+
<MacroExpansion>
58+
<BuildableReference
59+
BuildableIdentifier = "primary"
60+
BlueprintIdentifier = "915BC5D12E3CB9B50062B78E"
61+
BuildableName = "Hambug.app"
62+
BlueprintName = "Hambug"
63+
ReferencedContainer = "container:Hambug.xcodeproj">
64+
</BuildableReference>
65+
</MacroExpansion>
66+
</ProfileAction>
67+
<AnalyzeAction
68+
buildConfiguration = "Debug">
69+
</AnalyzeAction>
70+
<ArchiveAction
71+
buildConfiguration = "Release"
72+
revealArchiveInOrganizer = "YES">
73+
</ArchiveAction>
74+
</Scheme>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// TestUIBase.swift
3+
// Hambug
4+
//
5+
// Created by 강동영 on 2/7/26.
6+
//
7+
8+
import XCTest
9+
10+
class TestUIBase: XCTestCase {
11+
static var launched = false
12+
13+
var app: XCUIApplication?
14+
15+
convenience init(app: XCUIApplication?) {
16+
self.init()
17+
self.app = app
18+
}
19+
20+
override func setUp() {
21+
super.setUp()
22+
// 앱 Launch 여부(시간 단축용)
23+
if TestUIBase.launched == false {
24+
initApp(withLaunch: true)
25+
TestUIBase.launched = true
26+
} else {
27+
initApp(withLaunch: false)
28+
}
29+
}
30+
31+
private func initApp(withLaunch: Bool) {
32+
guard app == nil else { return }
33+
let application = XCUIApplication()
34+
if withLaunch {
35+
application.launchArguments.append("UITest")
36+
application.launch()
37+
}
38+
app = application
39+
}
40+
41+
override func tearDown() {
42+
super.tearDown()
43+
if let cnt = testRun?.failureCount, cnt > 0 {
44+
// 한번이라도 실패시 다시 Launch토록 설정
45+
TestUIBase.launched = false
46+
}
47+
}
48+
49+
enum DelayType: TimeInterval {
50+
case short = 0.7
51+
case medium = 1.3
52+
case long = 1.6
53+
}
54+
55+
/// 딜레이 걸기
56+
func delay(_ timeout: TimeInterval) {
57+
wait(for: [.delay], timeout: timeout)
58+
}
59+
func delay(_ type: DelayType) {
60+
wait(for: [.delay], timeout: type.rawValue)
61+
}
62+
63+
/// 스크린샷
64+
func takeScreenshot(name: String) {
65+
let fullScreenshot = XCUIScreen.main.screenshot()
66+
let screenshot = XCTAttachment(uniformTypeIdentifier: "public.png", name: "Screenshot-\(name)-\(UIDevice.current.name).png", payload: fullScreenshot.pngRepresentation, userInfo: nil)
67+
screenshot.lifetime = .keepAlways
68+
add(screenshot)
69+
}
70+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// XCTestExpectation+.swift
3+
// Hambug
4+
//
5+
// Created by 강동영 on 2/7/26.
6+
//
7+
8+
import XCTest
9+
10+
extension XCTestExpectation {
11+
static var delay: XCTestExpectation {
12+
let delay = XCTestExpectation()
13+
delay.isInverted = true
14+
return delay
15+
}
16+
}

0 commit comments

Comments
 (0)