mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
test: add tests for token authentication
This commit is contained in:
parent
d5e7ccdc4d
commit
6c171b2ae3
8 changed files with 140 additions and 18 deletions
|
|
@ -9,10 +9,12 @@ final class ForgejoInstance {
|
|||
var isDefault: Bool
|
||||
var lastUsed: Date
|
||||
var allowSelfSignedCertificates: Bool
|
||||
var useTokenAuth: Bool = false
|
||||
|
||||
init(
|
||||
serverURL: String, username: String, name: String = "",
|
||||
isDefault: Bool = false, allowSelfSignedCertificates: Bool = false,
|
||||
useTokenAuth: Bool = false,
|
||||
) {
|
||||
self.serverURL = serverURL
|
||||
self.username = username
|
||||
|
|
@ -20,5 +22,6 @@ final class ForgejoInstance {
|
|||
self.isDefault = isDefault
|
||||
lastUsed = Date()
|
||||
self.allowSelfSignedCertificates = allowSelfSignedCertificates
|
||||
self.useTokenAuth = useTokenAuth
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,16 +117,26 @@ class AuthenticationService {
|
|||
token: token,
|
||||
allowSelfSignedCertificates: instance.allowSelfSignedCertificates,
|
||||
)
|
||||
// Validate the token still works
|
||||
let user = try await tokenClient.fetchCurrentUser()
|
||||
client = tokenClient
|
||||
currentUser = user
|
||||
isAuthenticated = true
|
||||
currentInstance = instance
|
||||
return
|
||||
do {
|
||||
let user = try await tokenClient.fetchCurrentUser()
|
||||
client = tokenClient
|
||||
currentUser = user
|
||||
isAuthenticated = true
|
||||
currentInstance = instance
|
||||
return
|
||||
} catch {
|
||||
// Token is invalid/expired — only fall through to password if this is not a token-only instance
|
||||
if instance.useTokenAuth {
|
||||
throw SessionRestoreError.tokenExpired
|
||||
}
|
||||
// Otherwise fall through to password-based login below
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to password-based login (creates a new token)
|
||||
guard !instance.useTokenAuth else {
|
||||
throw SessionRestoreError.tokenExpired
|
||||
}
|
||||
let password = try await KeychainManager.shared.getPassword(for: normalizedURL, username: instance.username)
|
||||
try await login(
|
||||
serverURL: normalizedURL, username: instance.username,
|
||||
|
|
@ -152,3 +162,14 @@ class AuthenticationService {
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum SessionRestoreError: LocalizedError {
|
||||
case tokenExpired
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .tokenExpired:
|
||||
"Your API token is expired or has been revoked. Please edit this instance and enter a new token."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ struct InstanceFormView: View {
|
|||
@AppStorage("dev_serverURL") private var devServerURL = ""
|
||||
@AppStorage("dev_username") private var devUsername = ""
|
||||
@AppStorage("dev_password") private var devPassword = ""
|
||||
@AppStorage("dev_apiToken") private var devApiToken = ""
|
||||
@AppStorage("dev_allowSelfSigned") private var devAllowSelfSigned = false
|
||||
#endif
|
||||
|
||||
|
|
@ -96,19 +97,19 @@ struct InstanceFormView: View {
|
|||
} header: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Credentials")
|
||||
if isAddMode {
|
||||
Picker("Login Method", selection: $authMode) {
|
||||
ForEach(AuthMode.allCases, id: \.self) { mode in
|
||||
Text(mode.rawValue).tag(mode)
|
||||
}
|
||||
Picker("Login Method", selection: $authMode) {
|
||||
ForEach(AuthMode.allCases, id: \.self) { mode in
|
||||
Text(mode.rawValue).tag(mode)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.textCase(nil)
|
||||
.padding(.bottom, 4)
|
||||
} footer: {
|
||||
if authMode == .token {
|
||||
if authMode == .token, case .edit = mode {
|
||||
Text("Leave token blank to keep the existing one.")
|
||||
} else if authMode == .token {
|
||||
Text(
|
||||
"""
|
||||
Generate a token under Settings → Applications with the following scopes:
|
||||
|
|
@ -160,7 +161,7 @@ struct InstanceFormView: View {
|
|||
|| (authMode == .credentials
|
||||
&& (username.isEmpty || (isAddMode && password.isEmpty)
|
||||
|| (needsOTP && otpCode.isEmpty)))
|
||||
|| (authMode == .token && apiToken.isEmpty),
|
||||
|| (authMode == .token && isAddMode && apiToken.isEmpty),
|
||||
)
|
||||
.listRowBackground(Color.clear)
|
||||
.accessibilityIdentifier("login-button")
|
||||
|
|
@ -192,6 +193,7 @@ struct InstanceFormView: View {
|
|||
if !devServerURL.isEmpty { serverURL = devServerURL }
|
||||
if !devUsername.isEmpty { username = devUsername }
|
||||
if !devPassword.isEmpty { password = devPassword }
|
||||
if !devApiToken.isEmpty { apiToken = devApiToken }
|
||||
allowSelfSignedCertificates = devAllowSelfSigned
|
||||
#endif
|
||||
case .edit(let instance):
|
||||
|
|
@ -200,6 +202,7 @@ struct InstanceFormView: View {
|
|||
username = instance.username
|
||||
allowSelfSignedCertificates = instance.allowSelfSignedCertificates
|
||||
isDefault = instance.isDefault
|
||||
authMode = instance.useTokenAuth ? .token : .credentials
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,6 +260,7 @@ struct InstanceFormView: View {
|
|||
name: name,
|
||||
isDefault: isDefault,
|
||||
allowSelfSignedCertificates: allowSelfSignedCertificates,
|
||||
useTokenAuth: authMode == .token,
|
||||
)
|
||||
modelContext.insert(instance)
|
||||
do {
|
||||
|
|
@ -267,14 +271,18 @@ struct InstanceFormView: View {
|
|||
authService.currentInstance = instance
|
||||
|
||||
case .edit(let instance):
|
||||
if !password.isEmpty {
|
||||
if authMode == .token, !apiToken.isEmpty {
|
||||
try await performLogin()
|
||||
instance.username = authService.currentUser?.login ?? username
|
||||
} else if authMode == .credentials, !password.isEmpty {
|
||||
try await performLogin()
|
||||
instance.username = username
|
||||
}
|
||||
|
||||
instance.name = name
|
||||
instance.serverURL = ForgejoClient.normalizeServerURL(serverURL)
|
||||
instance.username = username
|
||||
instance.allowSelfSignedCertificates = allowSelfSignedCertificates
|
||||
instance.useTokenAuth = authMode == .token
|
||||
|
||||
if isDefault, !instance.isDefault {
|
||||
for inst in instances where inst.isDefault {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,20 @@ class ForgejoUITestBase: XCTestCase, UITestNavigating {
|
|||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
}
|
||||
|
||||
// MARK: - Test Token Resolution
|
||||
|
||||
/// Resolves the API token created by the seed script for token-based auth tests.
|
||||
static func resolveTestToken() -> String? {
|
||||
let deviceName = (ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] ?? "")
|
||||
.replacingOccurrences(of: " ", with: "_")
|
||||
let specificPath = "/tmp/forgejo_test_token_\(deviceName).txt"
|
||||
let genericPath = "/tmp/forgejo_test_token.txt"
|
||||
|
||||
return ((try? String(contentsOfFile: specificPath, encoding: .utf8))
|
||||
?? (try? String(contentsOfFile: genericPath, encoding: .utf8)))
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Launches the app and waits for HomeView to appear via auto-login.
|
||||
|
|
|
|||
|
|
@ -30,4 +30,71 @@ final class LoginUITests: ForgejoUITestBase {
|
|||
let alert = app.alerts["Login Failed"]
|
||||
XCTAssertTrue(alert.waitForExistence(timeout: 10))
|
||||
}
|
||||
|
||||
// MARK: - Token Login Success
|
||||
|
||||
@MainActor
|
||||
func testLoginWithToken() throws {
|
||||
guard let token = Self.resolveTestToken(), !token.isEmpty else {
|
||||
throw XCTSkip("No test API token available")
|
||||
}
|
||||
|
||||
app.launchArguments = [
|
||||
"-dev_serverURL", serverURL!,
|
||||
"-dev_apiToken", token,
|
||||
"-dev_skipAutoLogin", "true",
|
||||
]
|
||||
app.launch()
|
||||
|
||||
let addButton = app.buttons["instance-add-button"]
|
||||
XCTAssertTrue(addButton.waitForExistence(timeout: 15), "Instance list did not appear")
|
||||
addButton.tap()
|
||||
|
||||
// Switch to API Token auth mode
|
||||
let tokenSegment = app.buttons["API Token"]
|
||||
XCTAssertTrue(tokenSegment.waitForExistence(timeout: 5), "API Token segment not found")
|
||||
tokenSegment.tap()
|
||||
|
||||
// Scroll form to reveal login button
|
||||
app.swipeUp()
|
||||
|
||||
let loginButton = app.buttons["login-button"]
|
||||
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
|
||||
loginButton.tap()
|
||||
|
||||
// Verify we land on HomeView
|
||||
let reposTab = app.tabBars.buttons["Repositories"]
|
||||
XCTAssertTrue(reposTab.waitForExistence(timeout: 15), "HomeView did not appear after token login")
|
||||
}
|
||||
|
||||
// MARK: - Token Login Bad Token
|
||||
|
||||
@MainActor
|
||||
func testLoginWithBadToken() throws {
|
||||
app.launchArguments = [
|
||||
"-dev_serverURL", serverURL!,
|
||||
"-dev_apiToken", "invalid_token_value",
|
||||
"-dev_skipAutoLogin", "true",
|
||||
]
|
||||
app.launch()
|
||||
|
||||
let addButton = app.buttons["instance-add-button"]
|
||||
XCTAssertTrue(addButton.waitForExistence(timeout: 15), "Instance list did not appear")
|
||||
addButton.tap()
|
||||
|
||||
// Switch to API Token auth mode
|
||||
let tokenSegment = app.buttons["API Token"]
|
||||
XCTAssertTrue(tokenSegment.waitForExistence(timeout: 5), "API Token segment not found")
|
||||
tokenSegment.tap()
|
||||
|
||||
// Scroll form to reveal login button
|
||||
app.swipeUp()
|
||||
|
||||
let loginButton = app.buttons["login-button"]
|
||||
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
|
||||
loginButton.tap()
|
||||
|
||||
let alert = app.alerts["Login Failed"]
|
||||
XCTAssertTrue(alert.waitForExistence(timeout: 10))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
11b89bd63fe2a77529f2f8124efafaadcf4deccb435ae43957501aab0795fffd
|
||||
Binary file not shown.
|
|
@ -203,5 +203,15 @@ curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/4" \
|
|||
|
||||
wait
|
||||
|
||||
# --- Phase 6: Create API token for token-based auth tests ---
|
||||
echo "Creating API token for testadmin..."
|
||||
TOKEN_RESPONSE=$(curl -sf -X POST "$BASE_URL/api/v1/users/$ADMIN_USER/tokens" \
|
||||
-u "$ADMIN_AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "integration-test-token", "scopes": ["read:user", "write:user", "read:repository", "write:repository", "read:issue", "write:issue", "read:notification", "write:notification", "read:organization"]}')
|
||||
API_TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha1'])")
|
||||
echo "$API_TOKEN" > /tmp/forgejo_test_token.txt
|
||||
echo "API token written to /tmp/forgejo_test_token.txt"
|
||||
|
||||
echo ""
|
||||
echo "Seed data created successfully."
|
||||
|
|
|
|||
Loading…
Reference in a new issue