Skip to content

Commit

Permalink
SDK-2390 Prevent duplicate configure calls (#253)
Browse files Browse the repository at this point in the history
* Persist configuration options and short circuit if the options and token passed to configure haven't changed; Remove an extraneous debugging print

* Add/Update tests; Fix missing option persistence in B2B client
  • Loading branch information
jhaven-stytch authored Feb 7, 2025
1 parent a945312 commit eca82bd
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ public object StytchB2BClient {
@VisibleForTesting
internal lateinit var appSessionId: String

private var stytchClientOptions: StytchClientOptions? = null

/**
* This configures the API for authenticating requests and the encrypted storage helper for persisting session data
* across app launches.
Expand All @@ -125,9 +127,13 @@ public object StytchB2BClient {
options: StytchClientOptions = StytchClientOptions(),
callback: ((Boolean) -> Unit) = {},
) {
if (::publicToken.isInitialized && publicToken == this.publicToken && options == this.stytchClientOptions) {
return callback(true)
}
try {
deviceInfo = context.getDeviceInfo()
this.publicToken = publicToken
this.stytchClientOptions = options
appSessionId = "app-session-id-${UUID.randomUUID()}"
StorageHelper.initialize(context)
sessionStorage = B2BSessionStorage(StorageHelper)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public object StytchClient {
@VisibleForTesting
internal lateinit var appSessionId: String

private var stytchClientOptions: StytchClientOptions? = null

/**
* This configures the API for authenticating requests and the encrypted storage helper for persisting session data
* across app launches.
Expand All @@ -118,8 +120,12 @@ public object StytchClient {
options: StytchClientOptions = StytchClientOptions(),
callback: ((Boolean) -> Unit) = {},
) {
if (::publicToken.isInitialized && publicToken == this.publicToken && options == this.stytchClientOptions) {
return callback(true)
}
try {
this.publicToken = publicToken
this.stytchClientOptions = options
deviceInfo = context.getDeviceInfo()
appSessionId = "app-session-id-${UUID.randomUUID()}"
StorageHelper.initialize(context)
Expand Down Expand Up @@ -166,7 +172,6 @@ public object StytchClient {
callback(_isInitialized.value)
}
} catch (ex: Exception) {
println(ex)
events.logEvent("client_initialization_failure", null, ex)
throw StytchInternalError(
message = "Failed to initialize the SDK",
Expand Down
112 changes: 103 additions & 9 deletions source/sdk/src/test/java/com/stytch/sdk/b2b/StytchB2BClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.stytch.sdk.b2b.network.models.SessionsAuthenticateResponseData
import com.stytch.sdk.common.DeeplinkHandledStatus
import com.stytch.sdk.common.DeviceInfo
import com.stytch.sdk.common.EncryptionManager
import com.stytch.sdk.common.EndpointOptions
import com.stytch.sdk.common.PKCECodePair
import com.stytch.sdk.common.StorageHelper
import com.stytch.sdk.common.StytchClientOptions
Expand Down Expand Up @@ -51,6 +52,7 @@ import org.junit.Before
import org.junit.Test
import java.security.KeyStore
import java.util.Date
import java.util.UUID

internal class StytchB2BClientTest {
private var mContextMock = mockk<Context>(relaxed = true)
Expand Down Expand Up @@ -133,31 +135,32 @@ internal class StytchB2BClientTest {
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
val deviceInfo = DeviceInfo()
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(mContextMock, "")
verify { StytchB2BApi.configure("", deviceInfo) }
val publicToken = UUID.randomUUID().toString()
stytchClientObject.configure(mContextMock, publicToken)
verify { StytchB2BApi.configure(publicToken, deviceInfo) }
}

@Test
fun `should trigger StorageHelper initialize when calling StytchB2BClient configure`() {
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
val deviceInfo = DeviceInfo()
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(mContextMock, "")
stytchClientObject.configure(mContextMock, UUID.randomUUID().toString())
verify { StorageHelper.initialize(mContextMock) }
}

@Test
fun `should fetch bootstrap data when calling StytchB2BClient configure`() {
runBlocking {
StytchB2BClient.configure(mContextMock, "")
StytchB2BClient.configure(mContextMock, UUID.randomUUID().toString())
coVerify { StytchB2BApi.getBootstrapData() }
}
}

@Test
fun `configures DFP when calling StytchB2BClient configure`() {
runBlocking {
StytchB2BClient.configure(mContextMock, "")
StytchB2BClient.configure(mContextMock, UUID.randomUUID().toString())
verify(exactly = 1) { StytchB2BApi.configureDFP(any(), any(), any(), any()) }
}
}
Expand All @@ -172,7 +175,7 @@ internal class StytchB2BClientTest {
coEvery { StytchB2BApi.Sessions.authenticate(any()) } returns mockResponse
// no session data == no authentication/updater
every { StorageHelper.loadValue(any()) } returns null
StytchB2BClient.configure(mContextMock, "")
StytchB2BClient.configure(mContextMock, UUID.randomUUID().toString())
coVerify(exactly = 0) { StytchB2BApi.Sessions.authenticate(any()) }
verify(exactly = 0) { mockResponse.launchSessionUpdater(any(), any()) }
// yes session data, but expired, no authentication/updater
Expand All @@ -188,7 +191,7 @@ internal class StytchB2BClientTest {
.lenient()
.toJson(mockExpiredSession)
every { StorageHelper.loadValue(any()) } returns mockExpiredSessionJSON
StytchB2BClient.configure(mContextMock, "")
StytchB2BClient.configure(mContextMock, UUID.randomUUID().toString())
coVerify(exactly = 0) { StytchB2BApi.Sessions.authenticate() }
verify(exactly = 0) { mockResponse.launchSessionUpdater(any(), any()) }
// yes session data, and valid, yes authentication/updater
Expand All @@ -204,7 +207,7 @@ internal class StytchB2BClientTest {
.lenient()
.toJson(mockValidSession)
every { StorageHelper.loadValue(any()) } returns mockValidSessionJSON
StytchB2BClient.configure(mContextMock, "")
StytchB2BClient.configure(mContextMock, UUID.randomUUID().toString())
coVerify(exactly = 1) { StytchB2BApi.Sessions.authenticate() }
verify(exactly = 1) { mockResponse.launchSessionUpdater(any(), any()) }
}
Expand All @@ -219,7 +222,7 @@ internal class StytchB2BClientTest {
}
coEvery { StytchB2BApi.Sessions.authenticate(any()) } returns mockResponse
val callback = spyk<(Boolean) -> Unit>()
StytchB2BClient.configure(mContextMock, "", StytchClientOptions(), callback)
StytchB2BClient.configure(mContextMock, UUID.randomUUID().toString(), StytchClientOptions(), callback)
// callback is called with expected value
verify(exactly = 1) { callback(true) }
// isInitialized has fired
Expand All @@ -236,6 +239,97 @@ internal class StytchB2BClientTest {
stytchClientObject.configure(mContextMock, "")
}

@Test
fun `calling StytchB2BClient configure with the same public token and no options short circuits`() {
val deviceInfo = DeviceInfo()
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(mContextMock, "publicToken")
stytchClientObject.configure(mContextMock, "publicToken")
stytchClientObject.configure(mContextMock, "publicToken")
verify(exactly = 1) { mContextMock.getDeviceInfo() }
}

@Test
fun `calling StytchB2BClient configure with different public tokens and no options doesn't short circuit`() {
val deviceInfo = DeviceInfo()
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(mContextMock, "publicToken1")
stytchClientObject.configure(mContextMock, "publicToken2")
stytchClientObject.configure(mContextMock, "publicToken3")
verify(exactly = 3) { mContextMock.getDeviceInfo() }
}

@Test
fun `calling StytchB2BClient configure with the same public token and the same options short circuits`() {
val deviceInfo = DeviceInfo()
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(
mContextMock,
"publicToken",
StytchClientOptions(EndpointOptions("dfppa-domain.com")),
)
stytchClientObject.configure(
mContextMock,
"publicToken",
StytchClientOptions(EndpointOptions("dfppa-domain.com")),
)
stytchClientObject.configure(
mContextMock,
"publicToken",
StytchClientOptions(EndpointOptions("dfppa-domain.com")),
)
verify(exactly = 1) { mContextMock.getDeviceInfo() }
}

@Test
fun `calling StytchB2BClient configure with different public tokens and the same options doesn't short circuit`() {
val deviceInfo = DeviceInfo()
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(
mContextMock,
"publicToken1",
StytchClientOptions(EndpointOptions("dfppa-domain.com")),
)
stytchClientObject.configure(
mContextMock,
"publicToken2",
StytchClientOptions(EndpointOptions("dfppa-domain.com")),
)
stytchClientObject.configure(
mContextMock,
"publicToken3",
StytchClientOptions(EndpointOptions("dfppa-domain.com")),
)
verify(exactly = 3) { mContextMock.getDeviceInfo() }
}

@Test
fun `calling StytchB2BClient configure with the same public tokens and different options doesn't short circuit`() {
val deviceInfo = DeviceInfo()
val stytchClientObject = spyk<StytchB2BClient>(recordPrivateCalls = true)
every { mContextMock.getDeviceInfo() } returns deviceInfo
stytchClientObject.configure(
mContextMock,
"publicToken",
StytchClientOptions(EndpointOptions("dfppa-domain1.com")),
)
stytchClientObject.configure(
mContextMock,
"publicToken",
StytchClientOptions(EndpointOptions("dfppa-domain2.com")),
)
stytchClientObject.configure(
mContextMock,
"publicToken",
StytchClientOptions(EndpointOptions("dfppa-domain3.com")),
)
verify(exactly = 3) { mContextMock.getDeviceInfo() }
}

@Test(expected = StytchSDKNotConfiguredError::class)
fun `accessing StytchB2BClient magicLinks throws StytchSDKNotConfiguredError when not configured`() {
every { StytchB2BApi.isInitialized } returns false
Expand Down
Loading

0 comments on commit eca82bd

Please sign in to comment.