OXYGENHUB
# 0414607973cb oxygenhub-android / app / src / test / java / io / oxygenhub / android / api / DeviceFlowTest.kt
4.7 KiB · last changed The hub's API and device sign-in, the token in the Keystore (refs #2) 11 days ago source blame history raw
1
package io.oxygenhub.android.api
2
3
import io.oxygenhub.android.Fixtures
4
import kotlinx.coroutines.test.runTest
5
import kotlinx.serialization.json.jsonArray
6
import kotlinx.serialization.json.jsonObject
7
import kotlinx.serialization.json.jsonPrimitive
8
import okhttp3.OkHttpClient
9
import okhttp3.mockwebserver.MockResponse
10
import okhttp3.mockwebserver.MockWebServer
11
import org.junit.After
12
import org.junit.Assert.assertEquals
13
import org.junit.Assert.assertTrue
14
import org.junit.Assert.fail
15
import org.junit.Before
16
import org.junit.Test
17
18
class DeviceFlowTest {
19
    private lateinit var server: MockWebServer
20
    private val sleeps = mutableListOf<Long>()
21
22
    @Before
23
    fun setUp() {
24
        server = MockWebServer()
25
        server.start()
26
        sleeps.clear()
27
    }
28
29
    @After
30
    fun tearDown() {
31
        server.shutdown()
32
    }
33
34
    private fun auth() = HubDeviceAuth(server.url("/").toString().trimEnd('/'), OkHttpClient()) { sleeps += it }
35
36
    private fun answer(name: String, code: Int) = MockResponse().setResponseCode(code).setBody(Fixtures.text(name))
37
38
    private fun code() = HubJson.decodeFromString(DeviceCode.serializer(), Fixtures.text("device_code.json"))
39
40
    @Test
41
    fun requestingACodeNamesTheAppAndItsScopes() = runTest {
42
        server.enqueue(answer("device_code.json", 200))
43
        val code = auth().requestCode("OxygenHub for Android on Pixel 10 Pro Fold")
44
        assertEquals("BCDF-GHJK", code.userCode)
45
        val req = server.takeRequest()
46
        assertEquals("POST", req.method)
47
        assertEquals("/api/v1/device/code", req.path)
48
        assertEquals(null, req.getHeader("Authorization"))
49
        val body = HubJson.parseToJsonElement(req.body.readUtf8()).jsonObject
50
        assertEquals("OxygenHub for Android on Pixel 10 Pro Fold", body["client"]!!.jsonPrimitive.content)
51
        assertEquals(AppScopes, body["scopes"]!!.jsonArray.map { it.jsonPrimitive.content })
52
    }
53
54
    @Test
55
    fun aHubThatCannotSignInDevices() = runTest {
56
        server.enqueue(MockResponse().setResponseCode(400).setBody("""{"error":"invalid_scope","error_description":"unknown scope"}"""))
57
        try {
58
            auth().requestCode("app")
59
            fail("expected a refusal")
60
        } catch (e: DeviceFlowException.Refused) {
61
            assertEquals(400, e.status)
62
            assertTrue(e.message!!.endsWith("unknown scope"))
63
        }
64
    }
65
66
    @Test
67
    fun pollsUntilApprovedSlowingDownWhenTold() = runTest {
68
        server.enqueue(answer("device_authorization_pending.json", 400))
69
        server.enqueue(answer("device_slow_down.json", 400))
70
        server.enqueue(answer("device_authorization_pending.json", 400))
71
        server.enqueue(answer("device_token.json", 200))
72
        val grant = auth().awaitToken(code())
73
        assertEquals("oxy_test_not_a_real_token", grant.accessToken)
74
        assertEquals(listOf(5000L, 5000L, 10000L, 10000L), sleeps)
75
        repeat(4) {
76
            val req = server.takeRequest()
77
            assertEquals("/api/v1/device/token", req.path)
78
            val body = HubJson.parseToJsonElement(req.body.readUtf8()).jsonObject
79
            assertEquals(code().deviceCode, body["device_code"]!!.jsonPrimitive.content)
80
        }
81
    }
82
83
    @Test(expected = DeviceFlowException.Denied::class)
84
    fun denied() = runTest {
85
        server.enqueue(answer("device_access_denied.json", 400))
86
        auth().awaitToken(code())
87
    }
88
89
    @Test(expected = DeviceFlowException.Expired::class)
90
    fun expiredOnTheHub() = runTest {
91
        server.enqueue(answer("device_expired_token.json", 400))
92
        auth().awaitToken(code())
93
    }
94
95
    @Test
96
    fun expiresHereWhenTheHubNeverAnswers() = runTest {
97
        // A code with ten seconds to live, polled every five: two polls, then give up.
98
        repeat(5) { server.enqueue(answer("device_authorization_pending.json", 400)) }
99
        try {
100
            auth().awaitToken(code().copy(expiresIn = 10))
101
            fail("expected expiry")
102
        } catch (_: DeviceFlowException.Expired) {
103
        }
104
        assertEquals(2, server.requestCount)
105
    }
106
107
    @Test
108
    fun anUnexpectedAnswerStops() = runTest {
109
        server.enqueue(MockResponse().setResponseCode(500).setBody("""{"error":"server_error","error_description":"db down"}"""))
110
        try {
111
            auth().awaitToken(code())
112
            fail("expected a refusal")
113
        } catch (e: DeviceFlowException.Refused) {
114
            assertEquals(500, e.status)
115
            assertTrue(e.message!!.contains("db down"))
116
        }
117
    }
118
119
    @Test
120
    fun whoAmIUsesTheNewToken() = runTest {
121
        server.enqueue(answer("user.json", 200))
122
        assertEquals("andymccall", auth().whoAmI("oxy_new").username)
123
        val req = server.takeRequest()
124
        assertEquals("/api/v1/user", req.path)
125
        assertEquals("Bearer oxy_new", req.getHeader("Authorization"))
126
    }
127
}