Skip to content

Commit 0bf249d

Browse files
committed
Refcator ProfileLoader for testing + add tests
1 parent 0980c33 commit 0bf249d

2 files changed

Lines changed: 312 additions & 14 deletions

File tree

Sources/SotoCore/Credential/Login/ProfileConfigurationLoader.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ struct ProfileConfigurationLoader {
2222
/// - Parameters:
2323
/// - profileName: Name of the profile (defaults to "default")
2424
/// - cacheDirectoryOverride: Optional override for cache directory
25+
/// - configPath: Optional path to config file (defaults to ~/.aws/config)
2526
/// - Returns: LoginConfiguration
2627
func loadConfiguration(
2728
profileName: String? = nil,
28-
cacheDirectoryOverride: String? = nil
29+
cacheDirectoryOverride: String? = nil,
30+
configPath: String? = nil
2931
) throws -> LoginConfiguration {
3032
let profile = profileName ?? "default"
3133

32-
// Load profile from ~/.aws/config
33-
let profileData = try loadProfile(name: profile)
34+
// Load profile from config file
35+
let profileData = try loadProfile(name: profile, configPath: configPath)
3436

3537
// login_session is mandatory
3638
guard let loginSession = profileData["login_session"] else {
@@ -60,14 +62,19 @@ struct ProfileConfigurationLoader {
6062
)
6163
}
6264

63-
private func loadProfile(name: String) throws -> [String: String] {
64-
// Construct path to ~/.aws/config
65-
let homeDir = FileManager.default.homeDirectoryForCurrentUser
66-
let configPath = homeDir.appendingPathComponent(".aws").appendingPathComponent("config")
65+
private func loadProfile(name: String, configPath: String?) throws -> [String: String] {
66+
// Use provided path or construct default path to ~/.aws/config
67+
let path: String
68+
if let configPath = configPath {
69+
path = configPath
70+
} else {
71+
let homeDir = FileManager.default.homeDirectoryForCurrentUser
72+
path = homeDir.appendingPathComponent(".aws").appendingPathComponent("config").path
73+
}
6774

6875
// Read config file
69-
guard let configContent = try? String(contentsOf: configPath, encoding: .utf8) else {
70-
throw LoginError.configFileNotFound(configPath.path)
76+
guard let configContent = try? String(contentsOfFile: path, encoding: .utf8) else {
77+
throw LoginError.configFileNotFound(path)
7178
}
7279

7380
// Parse INI file

Tests/SotoCoreTests/Credential/Login/ProfileConfigurationLoaderTests.swift

Lines changed: 296 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,313 @@ import Testing
2020

2121
@testable import SotoCore
2222

23-
@Suite("Profile Configuration Loader")
23+
@Suite("Profile Configuration Loader", .serialized)
2424
struct ProfileConfigurationLoaderTests {
2525

26+
// Helper to create a temp config file
27+
func createTempConfigFile(content: String) throws -> String {
28+
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
29+
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
30+
let configPath = tempDir.appendingPathComponent("config")
31+
try content.write(to: configPath, atomically: true, encoding: .utf8)
32+
return configPath.path
33+
}
34+
35+
func removeTempFile(_ path: String) {
36+
let url = URL(fileURLWithPath: path)
37+
try? FileManager.default.removeItem(at: url.deletingLastPathComponent())
38+
}
39+
2640
@Test("Load configuration with missing profile throws error")
27-
func loadConfigurationMissingProfile() {
41+
func loadConfigurationMissingProfile() throws {
42+
let configContent = """
43+
[default]
44+
login_session = test-session
45+
"""
46+
47+
let configPath = try createTempConfigFile(content: configContent)
48+
defer { removeTempFile(configPath) }
49+
2850
let loader = ProfileConfigurationLoader()
2951

3052
// Trying to load a non-existent profile should throw
31-
#expect(throws: LoginError.self) {
53+
#expect(throws: LoginError.profileNotFound("nonexistent")) {
3254
try loader.loadConfiguration(
33-
profileName: "nonexistent-profile-12345",
34-
cacheDirectoryOverride: nil
55+
profileName: "nonexistent",
56+
cacheDirectoryOverride: nil,
57+
configPath: configPath
3558
)
3659
}
3760
}
3861

62+
@Test("Load configuration with valid default profile")
63+
func loadConfigurationDefaultProfile() throws {
64+
let configContent = """
65+
[default]
66+
login_session = test-session-123
67+
region = us-west-2
68+
"""
69+
70+
let configPath = try createTempConfigFile(content: configContent)
71+
defer { removeTempFile(configPath) }
72+
73+
let loader = ProfileConfigurationLoader()
74+
let config = try loader.loadConfiguration(
75+
profileName: nil,
76+
cacheDirectoryOverride: nil,
77+
configPath: configPath
78+
)
79+
80+
#expect(config.loginSession == "test-session-123")
81+
#expect(config.region == .uswest2)
82+
#expect(config.endpoint == "us-west-2.signin.aws.amazon.com")
83+
}
84+
85+
@Test("Load configuration with named profile")
86+
func loadConfigurationNamedProfile() throws {
87+
let configContent = """
88+
[default]
89+
login_session = default-session
90+
91+
[profile dev]
92+
login_session = dev-session-456
93+
region = eu-west-1
94+
"""
95+
96+
let configPath = try createTempConfigFile(content: configContent)
97+
defer { removeTempFile(configPath) }
98+
99+
let loader = ProfileConfigurationLoader()
100+
let config = try loader.loadConfiguration(
101+
profileName: "dev",
102+
cacheDirectoryOverride: nil,
103+
configPath: configPath
104+
)
105+
106+
#expect(config.loginSession == "dev-session-456")
107+
#expect(config.region == .euwest1)
108+
#expect(config.endpoint == "eu-west-1.signin.aws.amazon.com")
109+
}
110+
111+
@Test("Load configuration with missing login_session throws error")
112+
func loadConfigurationMissingLoginSession() throws {
113+
let configContent = """
114+
[default]
115+
region = us-east-1
116+
"""
117+
118+
let configPath = try createTempConfigFile(content: configContent)
119+
defer { removeTempFile(configPath) }
120+
121+
let loader = ProfileConfigurationLoader()
122+
123+
#expect(throws: LoginError.loginSessionMissing) {
124+
try loader.loadConfiguration(
125+
profileName: nil,
126+
cacheDirectoryOverride: nil,
127+
configPath: configPath
128+
)
129+
}
130+
}
131+
132+
@Test("Load configuration uses AWS_REGION env var when profile has no region")
133+
func loadConfigurationUsesEnvRegion() throws {
134+
let configContent = """
135+
[default]
136+
login_session = test-session
137+
"""
138+
139+
let configPath = try createTempConfigFile(content: configContent)
140+
defer { removeTempFile(configPath) }
141+
142+
let originalRegion = ProcessInfo.processInfo.environment["AWS_REGION"]
143+
setenv("AWS_REGION", "ap-southeast-1", 1)
144+
defer {
145+
if let region = originalRegion {
146+
setenv("AWS_REGION", region, 1)
147+
} else {
148+
unsetenv("AWS_REGION")
149+
}
150+
}
151+
152+
let loader = ProfileConfigurationLoader()
153+
let config = try loader.loadConfiguration(
154+
profileName: nil,
155+
cacheDirectoryOverride: nil,
156+
configPath: configPath
157+
)
158+
159+
#expect(config.region == .apsoutheast1)
160+
}
161+
162+
@Test("Load configuration defaults to us-east-1 when no region specified")
163+
func loadConfigurationDefaultsToUsEast1() throws {
164+
let configContent = """
165+
[default]
166+
login_session = test-session
167+
"""
168+
169+
let configPath = try createTempConfigFile(content: configContent)
170+
defer { removeTempFile(configPath) }
171+
172+
let originalRegion = ProcessInfo.processInfo.environment["AWS_REGION"]
173+
unsetenv("AWS_REGION")
174+
defer {
175+
if let region = originalRegion {
176+
setenv("AWS_REGION", region, 1)
177+
}
178+
}
179+
180+
let loader = ProfileConfigurationLoader()
181+
let config = try loader.loadConfiguration(
182+
profileName: nil,
183+
cacheDirectoryOverride: nil,
184+
configPath: configPath
185+
)
186+
187+
#expect(config.region == .useast1)
188+
}
189+
190+
@Test("Load configuration with cache directory override")
191+
func loadConfigurationWithCacheOverride() throws {
192+
let configContent = """
193+
[default]
194+
login_session = test-session
195+
region = us-east-1
196+
"""
197+
198+
let configPath = try createTempConfigFile(content: configContent)
199+
defer { removeTempFile(configPath) }
200+
201+
let loader = ProfileConfigurationLoader()
202+
let config = try loader.loadConfiguration(
203+
profileName: nil,
204+
cacheDirectoryOverride: "/custom/cache/dir",
205+
configPath: configPath
206+
)
207+
208+
#expect(config.cacheDirectory == "/custom/cache/dir")
209+
}
210+
211+
@Test("Load configuration with cache directory from environment")
212+
func loadConfigurationWithCacheFromEnv() throws {
213+
let configContent = """
214+
[default]
215+
login_session = test-session
216+
region = us-east-1
217+
"""
218+
219+
let configPath = try createTempConfigFile(content: configContent)
220+
defer { removeTempFile(configPath) }
221+
222+
let originalCache = ProcessInfo.processInfo.environment[LoginConfiguration.cacheEnvVar]
223+
setenv(LoginConfiguration.cacheEnvVar, "/env/cache/dir", 1)
224+
defer {
225+
if let cache = originalCache {
226+
setenv(LoginConfiguration.cacheEnvVar, cache, 1)
227+
} else {
228+
unsetenv(LoginConfiguration.cacheEnvVar)
229+
}
230+
}
231+
232+
let loader = ProfileConfigurationLoader()
233+
let config = try loader.loadConfiguration(
234+
profileName: nil,
235+
cacheDirectoryOverride: nil,
236+
configPath: configPath
237+
)
238+
239+
#expect(config.cacheDirectory == "/env/cache/dir")
240+
}
241+
242+
@Test("Load configuration with config file not found throws error")
243+
func loadConfigurationFileNotFound() {
244+
let loader = ProfileConfigurationLoader()
245+
246+
#expect(throws: LoginError.configFileNotFound("/nonexistent/config")) {
247+
try loader.loadConfiguration(
248+
profileName: nil,
249+
cacheDirectoryOverride: nil,
250+
configPath: "/nonexistent/config"
251+
)
252+
}
253+
}
254+
255+
@Test("Load configuration with multiple profiles")
256+
func loadConfigurationMultipleProfiles() throws {
257+
let configContent = """
258+
[default]
259+
login_session = default-session
260+
region = us-east-1
261+
262+
[profile staging]
263+
login_session = staging-session
264+
region = us-west-1
265+
266+
[profile production]
267+
login_session = prod-session
268+
region = eu-central-1
269+
"""
270+
271+
let configPath = try createTempConfigFile(content: configContent)
272+
defer { removeTempFile(configPath) }
273+
274+
let loader = ProfileConfigurationLoader()
275+
276+
// Test default profile
277+
let defaultConfig = try loader.loadConfiguration(
278+
profileName: nil,
279+
cacheDirectoryOverride: nil,
280+
configPath: configPath
281+
)
282+
#expect(defaultConfig.loginSession == "default-session")
283+
#expect(defaultConfig.region == .useast1)
284+
285+
// Test staging profile
286+
let stagingConfig = try loader.loadConfiguration(
287+
profileName: "staging",
288+
cacheDirectoryOverride: nil,
289+
configPath: configPath
290+
)
291+
#expect(stagingConfig.loginSession == "staging-session")
292+
#expect(stagingConfig.region == .uswest1)
293+
294+
// Test production profile
295+
let prodConfig = try loader.loadConfiguration(
296+
profileName: "production",
297+
cacheDirectoryOverride: nil,
298+
configPath: configPath
299+
)
300+
#expect(prodConfig.loginSession == "prod-session")
301+
#expect(prodConfig.region == .eucentral1)
302+
}
303+
304+
@Test("Load configuration with profile containing extra fields")
305+
func loadConfigurationWithExtraFields() throws {
306+
let configContent = """
307+
[default]
308+
login_session = test-session
309+
region = us-east-1
310+
output = json
311+
cli_pager =
312+
some_other_field = value
313+
"""
314+
315+
let configPath = try createTempConfigFile(content: configContent)
316+
defer { removeTempFile(configPath) }
317+
318+
let loader = ProfileConfigurationLoader()
319+
let config = try loader.loadConfiguration(
320+
profileName: nil,
321+
cacheDirectoryOverride: nil,
322+
configPath: configPath
323+
)
324+
325+
// Should successfully load despite extra fields
326+
#expect(config.loginSession == "test-session")
327+
#expect(config.region == .useast1)
328+
}
329+
39330
@Test(
40331
"Endpoint construction for various regions",
41332
arguments: [

0 commit comments

Comments
 (0)