mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
### Summary of Changes Previously, on the Swift SDK, the `signInWithOAuth` function wasn't working. In this PR, we fix it by having the `getOAuthUrl` function to actually redirect correctly. Note that to do so, we updated the `validRedirectUrl` check on the backend to accept app native redirects (from our new trusted url scheme). Another thing to note is that we added functionality to the `TokenStore` abstraction to conditionally refresh the access token that the user is trying to fetch if it is expired/close to expiring if possible. `getOAuthUrl` will attempt to get a valid access token, and thus will rely on our algorithm documented in `utilities.md`. The specs serve as the source of truth. We go further and implement Apple Native sign in. To do so, we have it hit a new route on the backend and verify the `jwtToken` retrieved by the sdk against an Apple-provided set of `jwks`. We use jose to do so, in line with the rest of the codebase. We take this opportunity to refactor the oauth provider route owing to the amount of duplicated logic. Additionally, to enable the apple sign in, users will have to update the Apple authentication method modal on the dashboard and add accepted bundle ids. These are identifiers for projects, and we will check the `JWT` on the backend to make sure the audience is set to an accepted bundleId. We also update the Apple modal to be more informative. ### Using the new Features To use the Apple native sign in, users will have to 1) sign up with an apple developer account, 2) set up their bundleids for their projects by connecting them to the apple developer account, 3) update the Stack-Auth Authentication Methods dashboard apple modal with the relevant fields. Then, trying to sign in with apple with our Swift SDK will use the apple native sign in. ### UI Changes Renamed the fields in the apple modal. Added a new field for bundle ids. See below. https://github.com/user-attachments/assets/0e760c0e-3198-4818-ac7f-4900d7a125bb Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
249 lines
8.7 KiB
Swift
249 lines
8.7 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import StackAuth
|
|
|
|
@Suite("Error Handling Tests")
|
|
struct ErrorHandlingTests {
|
|
|
|
// MARK: - Authentication Errors
|
|
|
|
@Test("Should throw EmailPasswordMismatchError for wrong credentials")
|
|
func emailPasswordMismatchError() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
do {
|
|
try await app.signInWithCredential(email: "nonexistent@example.com", password: "wrong")
|
|
Issue.record("Expected EmailPasswordMismatchError")
|
|
} catch is EmailPasswordMismatchError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "EMAIL_PASSWORD_MISMATCH" {
|
|
// Also acceptable
|
|
}
|
|
}
|
|
|
|
@Test("Should throw UserWithEmailAlreadyExistsError for duplicate sign up")
|
|
func userAlreadyExistsError() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
try await app.signOut()
|
|
|
|
do {
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
Issue.record("Expected UserWithEmailAlreadyExistsError")
|
|
} catch is UserWithEmailAlreadyExistsError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "USER_EMAIL_ALREADY_EXISTS" {
|
|
// Also acceptable
|
|
}
|
|
}
|
|
|
|
@Test("Should throw UserNotSignedInError for unauthenticated access")
|
|
func userNotSignedInError() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
await #expect(throws: UserNotSignedInError.self) {
|
|
_ = try await app.getUser(or: .throw)
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Properties
|
|
|
|
@Test("Should include error code in error")
|
|
func errorIncludesCode() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
do {
|
|
try await app.signInWithCredential(email: "nonexistent@example.com", password: "wrong")
|
|
Issue.record("Expected error")
|
|
} catch let error as StackAuthErrorProtocol {
|
|
#expect(!error.code.isEmpty)
|
|
#expect(error.code == "EMAIL_PASSWORD_MISMATCH")
|
|
}
|
|
}
|
|
|
|
@Test("Should include error message in error")
|
|
func errorIncludesMessage() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
do {
|
|
try await app.signInWithCredential(email: "nonexistent@example.com", password: "wrong")
|
|
Issue.record("Expected error")
|
|
} catch let error as StackAuthErrorProtocol {
|
|
#expect(!error.message.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test("Should have meaningful error description")
|
|
func errorHasMeaningfulDescription() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
do {
|
|
try await app.signInWithCredential(email: "nonexistent@example.com", password: "wrong")
|
|
Issue.record("Expected error")
|
|
} catch let error as StackAuthErrorProtocol {
|
|
let description = error.description
|
|
#expect(!description.isEmpty)
|
|
#expect(description.contains("EMAIL_PASSWORD_MISMATCH") || description.contains("password"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Type Matching
|
|
|
|
@Test("Should match StackAuthError for unknown error codes")
|
|
func unknownErrorCodeMatchesStackAuthError() async throws {
|
|
// Create a StackAuthError with unknown code
|
|
let error = StackAuthError(code: "UNKNOWN_ERROR_CODE", message: "Test error")
|
|
|
|
#expect(error.code == "UNKNOWN_ERROR_CODE")
|
|
#expect(error.message == "Test error")
|
|
}
|
|
|
|
@Test("Should properly identify specific error types")
|
|
func identifySpecificErrorTypes() async throws {
|
|
let emailError = EmailPasswordMismatchError()
|
|
let userExistsError = UserWithEmailAlreadyExistsError()
|
|
let notSignedInError = UserNotSignedInError()
|
|
|
|
#expect(emailError.code == "EMAIL_PASSWORD_MISMATCH")
|
|
#expect(userExistsError.code == "USER_EMAIL_ALREADY_EXISTS")
|
|
#expect(notSignedInError.code == "USER_NOT_SIGNED_IN")
|
|
}
|
|
|
|
// MARK: - Error Recovery
|
|
|
|
@Test("Should be able to retry after authentication error")
|
|
func retryAfterAuthError() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
// Sign up
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
try await app.signOut()
|
|
|
|
// First try with wrong password
|
|
do {
|
|
try await app.signInWithCredential(email: email, password: "WrongPassword123!")
|
|
} catch is EmailPasswordMismatchError {
|
|
// Expected
|
|
}
|
|
|
|
// Should still be able to sign in with correct password
|
|
try await app.signInWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let user = try await app.getUser()
|
|
#expect(user != nil)
|
|
}
|
|
|
|
// MARK: - Server-Side Errors
|
|
|
|
@Test("Should handle user not found for server operations")
|
|
func serverUserNotFound() async throws {
|
|
let app = TestConfig.createServerApp()
|
|
|
|
let fakeUserId = UUID().uuidString
|
|
let user = try await app.getUser(id: fakeUserId)
|
|
|
|
// Should return nil, not throw
|
|
#expect(user == nil)
|
|
}
|
|
|
|
@Test("Should handle team not found for server operations")
|
|
func serverTeamNotFound() async throws {
|
|
let app = TestConfig.createServerApp()
|
|
|
|
let fakeTeamId = UUID().uuidString
|
|
let team = try await app.getTeam(id: fakeTeamId)
|
|
|
|
// Should return nil, not throw
|
|
#expect(team == nil)
|
|
}
|
|
|
|
// MARK: - Password Errors
|
|
|
|
@Test("Should throw for weak password")
|
|
func weakPasswordError() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
do {
|
|
try await app.signUpWithCredential(email: email, password: "123")
|
|
Issue.record("Expected password error")
|
|
} catch is PasswordRequirementsNotMetError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "PASSWORD_REQUIREMENTS_NOT_MET" || error.code == "PASSWORD_TOO_SHORT" {
|
|
// Also acceptable - different error codes for password issues
|
|
}
|
|
}
|
|
|
|
@Test("Should throw PasswordConfirmationMismatchError for wrong old password")
|
|
func wrongOldPasswordError() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let user = try await app.getUser()
|
|
|
|
do {
|
|
try await user?.updatePassword(oldPassword: "WrongOld123!", newPassword: "NewPass456!")
|
|
Issue.record("Expected PasswordConfirmationMismatchError")
|
|
} catch is PasswordConfirmationMismatchError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "PASSWORD_CONFIRMATION_MISMATCH" {
|
|
// Also acceptable
|
|
}
|
|
}
|
|
}
|
|
|
|
@Suite("Project Tests")
|
|
struct ProjectTests {
|
|
|
|
// MARK: - Project Info Tests
|
|
|
|
@Test("Should get project info via client")
|
|
func getProjectViaClient() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
let project = try await app.getProject()
|
|
|
|
#expect(project.id == testProjectId)
|
|
}
|
|
|
|
@Test("Should get project info via server")
|
|
func getProjectViaServer() async throws {
|
|
let app = TestConfig.createServerApp()
|
|
|
|
let project = try await app.getProject()
|
|
|
|
#expect(project.id == testProjectId)
|
|
}
|
|
|
|
@Test("Should access project config")
|
|
func accessProjectConfig() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
let project = try await app.getProject()
|
|
|
|
// Config should exist (even if empty)
|
|
let _ = project.config
|
|
}
|
|
|
|
@Test("Should create client app with correct project ID")
|
|
func createClientAppWithProjectId() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
let projectId = await app.projectId
|
|
#expect(projectId == testProjectId)
|
|
}
|
|
|
|
@Test("Should create server app with correct project ID")
|
|
func createServerAppWithProjectId() async throws {
|
|
let app = TestConfig.createServerApp()
|
|
|
|
let projectId = await app.projectId
|
|
#expect(projectId == testProjectId)
|
|
}
|
|
}
|