-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Begin adding some unit tests for Bluesky models.
- Loading branch information
1 parent
f26d1b2
commit ea145d3
Showing
10 changed files
with
350 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
bluesky/src/commonTest/kotlin/sh/christian/ozone/api/AtpErrorsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package sh.christian.ozone.api | ||
|
||
import com.atproto.server.DescribeServerLinks | ||
import com.atproto.server.DescribeServerResponse | ||
import io.ktor.client.HttpClient | ||
import io.ktor.http.HttpStatusCode.Companion.BadRequest | ||
import io.ktor.http.HttpStatusCode.Companion.OK | ||
import kotlinx.coroutines.test.runTest | ||
import sh.christian.ozone.XrpcBlueskyApi | ||
import sh.christian.ozone.api.response.AtpErrorDescription | ||
import sh.christian.ozone.api.response.AtpResponse | ||
import sh.christian.ozone.api.response.StatusCode.InvalidRequest | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertIs | ||
import kotlin.test.assertNull | ||
|
||
class AtpErrorsTest { | ||
|
||
private val describeServerResponse = DescribeServerResponse( | ||
did = Did("did:web:bsky.social"), | ||
availableUserDomains = listOf(".bsky.social"), | ||
inviteCodeRequired = false, | ||
phoneVerificationRequired = true, | ||
links = DescribeServerLinks( | ||
privacyPolicy = Uri("https://blueskyweb.xyz/support/privacy-policy"), | ||
termsOfService = Uri("https://blueskyweb.xyz/support/tos"), | ||
), | ||
) | ||
|
||
private val errorDescription = AtpErrorDescription( | ||
error = "AuthFactorTokenRequired", | ||
message = "Needs 2FA", | ||
) | ||
|
||
@Test | ||
fun testSuccessHasNoError() = runTest { | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(statusCode = OK, response = describeServerResponse))) | ||
|
||
val response = api.describeServer() | ||
assertIs<AtpResponse.Success<DescribeServerResponse>>(response) | ||
assertEquals(response.response, describeServerResponse) | ||
} | ||
|
||
@Test | ||
fun testEmptyError() = runTest { | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(statusCode = BadRequest, response = ""))) | ||
|
||
val response = api.describeServer() | ||
assertIs<AtpResponse.Failure<DescribeServerResponse>>(response) | ||
assertNull(response.response) | ||
assertNull(response.error) | ||
} | ||
|
||
@Test | ||
fun testErrorKeepsStatusCode() = runTest { | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(statusCode = BadRequest, response = ""))) | ||
|
||
val response = api.describeServer() | ||
assertIs<AtpResponse.Failure<DescribeServerResponse>>(response) | ||
assertEquals(InvalidRequest, response.statusCode) | ||
} | ||
|
||
@Test | ||
fun testErrorKeepsHeaders() = runTest { | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(statusCode = BadRequest, response = ""))) | ||
|
||
val response = api.describeServer() | ||
assertIs<AtpResponse.Failure<DescribeServerResponse>>(response) | ||
assertEquals(mapOf("Content-Type" to "application/json"), response.headers) | ||
} | ||
|
||
@Test | ||
fun testFailureWithResponse() = runTest { | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(statusCode = BadRequest, response = describeServerResponse))) | ||
|
||
val response = api.describeServer() | ||
assertIs<AtpResponse.Failure<DescribeServerResponse>>(response) | ||
assertEquals(InvalidRequest, response.statusCode) | ||
assertNull(response.error) | ||
assertEquals(response.response, describeServerResponse) | ||
} | ||
|
||
@Test | ||
fun testFailureWithAtpErrorDescription() = runTest { | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(statusCode = BadRequest, error = errorDescription))) | ||
|
||
val response = api.describeServer() | ||
assertIs<AtpResponse.Failure<DescribeServerResponse>>(response) | ||
assertEquals(InvalidRequest, response.statusCode) | ||
assertEquals(response.error, errorDescription) | ||
assertNull(response.response) | ||
} | ||
} |
190 changes: 190 additions & 0 deletions
190
bluesky/src/commonTest/kotlin/sh/christian/ozone/api/AuthenticatedXrpcBlueskyApiTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
package sh.christian.ozone.api | ||
|
||
import com.atproto.server.CreateAccountRequest | ||
import com.atproto.server.CreateAccountResponse | ||
import com.atproto.server.CreateSessionRequest | ||
import com.atproto.server.CreateSessionResponse | ||
import com.atproto.server.RefreshSessionResponse | ||
import io.ktor.client.HttpClient | ||
import kotlinx.coroutines.test.runTest | ||
import kotlinx.serialization.encodeToString | ||
import sh.christian.ozone.BlueskyJson | ||
import sh.christian.ozone.XrpcBlueskyApi | ||
import sh.christian.ozone.api.AuthenticatedXrpcBlueskyApi.Companion.authenticated | ||
import sh.christian.ozone.api.response.AtpErrorDescription | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertNull | ||
|
||
class AuthenticatedXrpcBlueskyApiTest { | ||
|
||
@Test | ||
fun testInitialTokensFromConstructor() = runTest { | ||
val tokens = BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt") | ||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine("")), tokens) | ||
assertEquals(tokens, api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testInitialTokensFromFactory() = runTest { | ||
val tokens = BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt") | ||
val api = XrpcBlueskyApi(HttpClient(mockEngine(""))).authenticated(tokens) | ||
assertEquals(tokens, api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testCreateAccount() = runTest { | ||
val request = CreateAccountRequest( | ||
email = "bob@gmail.com", | ||
handle = Handle("bob.bsky.social"), | ||
password = "password", | ||
) | ||
val response = CreateAccountResponse( | ||
accessJwt = "accessJwt", | ||
refreshJwt = "refreshJwt", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine(response))) | ||
assertNull(api.authTokens.value) | ||
api.createAccount(request) | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt"), api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testCreateSession() = runTest { | ||
val request = CreateSessionRequest( | ||
identifier = "bob.bsky.social", | ||
password = "password", | ||
) | ||
val response = CreateSessionResponse( | ||
accessJwt = "accessJwt", | ||
refreshJwt = "refreshJwt", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine(response))) | ||
assertNull(api.authTokens.value) | ||
api.createSession(request) | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt"), api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testRefreshSession() = runTest { | ||
val response = RefreshSessionResponse( | ||
accessJwt = "accessJwt", | ||
refreshJwt = "refreshJwt", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine(response))) | ||
assertNull(api.authTokens.value) | ||
api.refreshSession() | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt"), api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testCreateThenRefreshSession() = runTest { | ||
val createRequest = CreateSessionRequest( | ||
identifier = "bob.bsky.social", | ||
password = "password", | ||
) | ||
val createResponse = CreateSessionResponse( | ||
accessJwt = "accessJwt-1", | ||
refreshJwt = "refreshJwt-1", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
val refreshResponse = RefreshSessionResponse( | ||
accessJwt = "accessJwt-2", | ||
refreshJwt = "refreshJwt-2", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine { request -> | ||
when (request.url.encodedPath) { | ||
"/xrpc/com.atproto.server.createSession" -> BlueskyJson.encodeToString(createResponse) | ||
"/xrpc/com.atproto.server.refreshSession" -> BlueskyJson.encodeToString(refreshResponse) | ||
else -> error("Unexpected request: ${request.url}") | ||
} | ||
})) | ||
|
||
assertNull(api.authTokens.value) | ||
api.createSession(createRequest) | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt-1", "refreshJwt-1"), api.authTokens.value) | ||
api.refreshSession() | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt-2", "refreshJwt-2"), api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testDeleteSession() = runTest { | ||
val createRequest = CreateSessionRequest( | ||
identifier = "bob.bsky.social", | ||
password = "password", | ||
) | ||
val createResponse = CreateSessionResponse( | ||
accessJwt = "accessJwt", | ||
refreshJwt = "refreshJwt", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine { request -> | ||
when (request.url.encodedPath) { | ||
"/xrpc/com.atproto.server.createSession" -> BlueskyJson.encodeToString(createResponse) | ||
"/xrpc/com.atproto.server.deleteSession" -> "" | ||
else -> error("Unexpected request: ${request.url}") | ||
} | ||
})) | ||
|
||
assertNull(api.authTokens.value) | ||
api.createSession(createRequest) | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt"), api.authTokens.value) | ||
api.deleteSession() | ||
assertNull(api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testClearCredentials() = runTest { | ||
val createRequest = CreateSessionRequest( | ||
identifier = "bob.bsky.social", | ||
password = "password", | ||
) | ||
val createResponse = CreateSessionResponse( | ||
accessJwt = "accessJwt", | ||
refreshJwt = "refreshJwt", | ||
handle = Handle("bob.bsky.social"), | ||
did = Did("did:plc:123"), | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine(createResponse))) | ||
|
||
assertNull(api.authTokens.value) | ||
api.createSession(createRequest) | ||
assertEquals(BlueskyAuthPlugin.Tokens("accessJwt", "refreshJwt"), api.authTokens.value) | ||
api.clearCredentials() | ||
assertNull(api.authTokens.value) | ||
} | ||
|
||
@Test | ||
fun testFailingNetworkCallDoesNotSave() = runTest { | ||
val createRequest = CreateSessionRequest( | ||
identifier = "bob.bsky.social", | ||
password = "password", | ||
) | ||
val error = AtpErrorDescription( | ||
error = "AuthFactorTokenRequired", | ||
message = "Needs 2FA", | ||
) | ||
|
||
val api = AuthenticatedXrpcBlueskyApi(HttpClient(mockEngine(error))) | ||
|
||
assertNull(api.authTokens.value) | ||
api.createSession(createRequest) | ||
assertNull(api.authTokens.value) | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
bluesky/src/commonTest/kotlin/sh/christian/ozone/api/mockEngine.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package sh.christian.ozone.api | ||
|
||
import io.ktor.client.engine.mock.MockEngine | ||
import io.ktor.client.engine.mock.respond | ||
import io.ktor.client.request.HttpRequestData | ||
import io.ktor.http.HttpHeaders | ||
import io.ktor.http.HttpStatusCode | ||
import io.ktor.http.headersOf | ||
import io.ktor.utils.io.ByteReadChannel | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.encodeToString | ||
import sh.christian.ozone.BlueskyJson | ||
import sh.christian.ozone.api.response.AtpErrorDescription | ||
|
||
inline fun <reified T : @Serializable Any> mockEngine( | ||
response: T, | ||
statusCode: HttpStatusCode = HttpStatusCode.OK, | ||
): MockEngine { | ||
return mockEngine(statusCode = statusCode) { BlueskyJson.encodeToString(response) } | ||
} | ||
|
||
inline fun mockEngine( | ||
error: AtpErrorDescription, | ||
statusCode: HttpStatusCode = HttpStatusCode.BadRequest, | ||
): MockEngine { | ||
return mockEngine(response = error, statusCode = statusCode) | ||
} | ||
|
||
inline fun mockEngine( | ||
statusCode: HttpStatusCode = HttpStatusCode.OK, | ||
noinline responseProvider: (HttpRequestData) -> String, | ||
): MockEngine { | ||
return MockEngine { request -> | ||
respond( | ||
content = ByteReadChannel(responseProvider(request)), | ||
status = statusCode, | ||
headers = headersOf(HttpHeaders.ContentType, "application/json"), | ||
) | ||
} | ||
} |
Oops, something went wrong.