stack/sdks/implementations/swift/Tests/StackAuthTests/ContactChannelTests.swift
Aman Ganapathy c8694c7ff5
[Fix] [Feat] Update OAuth Sign-In and Get Token Functions to Work (#1130)
### 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>
2026-01-28 02:17:27 +00:00

183 lines
5.8 KiB
Swift

import Testing
import Foundation
@testable import StackAuth
@Suite("Contact Channel Tests")
struct ContactChannelTests {
// MARK: - List Contact Channels Tests
@Test("Should list contact channels after sign up")
func listContactChannelsAfterSignUp() 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()
let channels = try await user?.listContactChannels() ?? []
// Should have at least the primary email
#expect(!channels.isEmpty)
// Find the primary email channel
var primaryChannel: ContactChannel? = nil
for channel in channels {
let channelValue = await channel.value
let channelIsPrimary = await channel.isPrimary
if channelValue == email && channelIsPrimary {
primaryChannel = channel
break
}
}
#expect(primaryChannel != nil)
}
@Test("Should have correct contact channel properties")
func contactChannelProperties() 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()
let channels = try await user?.listContactChannels() ?? []
guard let channel = channels.first else {
Issue.record("Expected at least one contact channel")
return
}
let channelId = channel.id // nonisolated, no await needed
let channelType = await channel.type
let channelValue = await channel.value
#expect(!channelId.isEmpty)
#expect(channelType == "email")
#expect(!channelValue.isEmpty)
}
@Test("Should identify primary contact channel")
func identifyPrimaryContactChannel() 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()
let channels = try await user?.listContactChannels() ?? []
// Count primary channels
var primaryCount = 0
var primaryValue: String? = nil
for channel in channels {
let isPrimary = await channel.isPrimary
if isPrimary {
primaryCount += 1
primaryValue = await channel.value
}
}
#expect(primaryCount == 1)
#expect(primaryValue == email)
}
// MARK: - Contact Channel via Server
@Test("Should list contact channels via server")
func listContactChannelsViaServer() async throws {
let app = TestConfig.createServerApp()
let email = TestConfig.uniqueEmail()
let user = try await app.createUser(email: email)
let channels = try await user.listContactChannels()
#expect(!channels.isEmpty)
// Find the email channel
var foundChannel: ContactChannel? = nil
for channel in channels {
let channelValue = await channel.value
if channelValue == email {
foundChannel = channel
break
}
}
#expect(foundChannel != nil)
// Clean up
try await user.delete()
}
@Test("Should handle user with no contact channels")
func userWithNoContactChannels() async throws {
let app = TestConfig.createServerApp()
// Create user without email
let user = try await app.createUser(displayName: "No Email User")
let channels = try await user.listContactChannels()
// Should be empty
#expect(channels.isEmpty)
// Clean up
try await user.delete()
}
@Test("Should show verified status correctly")
func verifiedStatusCorrect() async throws {
let app = TestConfig.createServerApp()
let email = TestConfig.uniqueEmail()
// Create user with verified email
let user = try await app.createUser(email: email, primaryEmailVerified: true)
let channels = try await user.listContactChannels()
// Find the email channel
var emailChannel: ContactChannel? = nil
for channel in channels {
let channelValue = await channel.value
if channelValue == email {
emailChannel = channel
break
}
}
let isVerified = await emailChannel?.isVerified
#expect(isVerified == true)
// Clean up
try await user.delete()
}
@Test("Should show unverified status correctly")
func unverifiedStatusCorrect() async throws {
let app = TestConfig.createServerApp()
let email = TestConfig.uniqueEmail()
// Create user with unverified email (default)
let user = try await app.createUser(email: email, primaryEmailVerified: false)
let channels = try await user.listContactChannels()
// Find the email channel
var emailChannel: ContactChannel? = nil
for channel in channels {
let channelValue = await channel.value
if channelValue == email {
emailChannel = channel
break
}
}
let isVerified = await emailChannel?.isVerified
#expect(isVerified == false)
// Clean up
try await user.delete()
}
}