pakstr 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android-template/app/src/androidTest/java/com/pakstr/app/Nip55CallbackTest.kt +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +12 -28
- package/android-template/app/src/main/java/com/pakstr/app/Nip55Callback.kt +3 -0
- package/android-template/app/src/main/java/com/pakstr/app/Nip55SignEventRequest.kt +303 -0
- package/android-template/app/src/main/java/com/pakstr/app/PakstrBridge.kt +14 -2
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseSettings.kt +98 -0
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseValidator.kt +30 -0
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeConfig.kt +7 -12
- package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +67 -17
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +70 -4
- package/android-template/app/src/main/res/layout/activity_developer_tools.xml +257 -66
- package/android-template/app/src/main/res/values/strings.xml +24 -5
- package/android-template/app/src/test/java/com/pakstr/app/Nip55SignEventDispatchTest.kt +115 -0
- package/android-template/app/src/test/java/com/pakstr/app/Nip55SignEventRequestTest.kt +90 -0
- package/android-template/app/src/test/java/com/pakstr/app/PakstrBridgeRuntimeApiBaseTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseProxyTest.kt +49 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseSettingsTest.kt +107 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseValidatorTest.kt +50 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.signer.SignerType
|
|
4
|
+
import kotlinx.coroutines.runBlocking
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import org.junit.Assert.assertEquals
|
|
8
|
+
import org.junit.Assert.assertFalse
|
|
9
|
+
import org.junit.Assert.assertTrue
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
|
|
12
|
+
class Nip55SignEventDispatchTest {
|
|
13
|
+
private val callback = "pakstr-test://nip55-callback#nip55_event="
|
|
14
|
+
private val activePubkey = "1".repeat(64)
|
|
15
|
+
|
|
16
|
+
@Test
|
|
17
|
+
fun bunker_valid_sign_event_signs_internally_without_external_intent() {
|
|
18
|
+
val result = dispatch(SignerType.BUNKER, validRequest())
|
|
19
|
+
|
|
20
|
+
assertTrue(result.signed)
|
|
21
|
+
assertFalse(result.forwarded)
|
|
22
|
+
assertEquals(SIGNED_EVENT, result.delivered)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun bunker_invalid_sign_event_is_consumed_without_external_intent() {
|
|
27
|
+
val result = dispatch(
|
|
28
|
+
SignerType.BUNKER,
|
|
29
|
+
validRequest().replace("returnType=event", "returnType=signature")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assertFalse(result.signed)
|
|
33
|
+
assertFalse(result.forwarded)
|
|
34
|
+
assertTrue(result.rejection != null)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun bunker_other_operation_is_forwarded_externally() {
|
|
39
|
+
val result = dispatch(
|
|
40
|
+
SignerType.BUNKER,
|
|
41
|
+
validRequest().replace("type=sign_event", "type=get_public_key")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assertFalse(result.signed)
|
|
45
|
+
assertTrue(result.forwarded)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun amber_sign_event_is_forwarded_externally() {
|
|
50
|
+
val result = dispatch(SignerType.AMBER, validRequest())
|
|
51
|
+
|
|
52
|
+
assertFalse(result.signed)
|
|
53
|
+
assertTrue(result.forwarded)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun dispatch(type: SignerType, rawUrl: String): Result {
|
|
57
|
+
var forwarded = false
|
|
58
|
+
var signed = false
|
|
59
|
+
var delivered: String? = null
|
|
60
|
+
var rejection: String? = null
|
|
61
|
+
|
|
62
|
+
dispatchNip55SignEvent(
|
|
63
|
+
rawUrl = rawUrl,
|
|
64
|
+
activeSignerType = type,
|
|
65
|
+
expectedCallbackUrl = callback,
|
|
66
|
+
forwardExternally = { forwarded = true },
|
|
67
|
+
launchSigning = { operation -> runBlocking { operation() } },
|
|
68
|
+
getActivePubkey = { activePubkey },
|
|
69
|
+
signEvent = {
|
|
70
|
+
signed = true
|
|
71
|
+
SIGNED_EVENT
|
|
72
|
+
},
|
|
73
|
+
deliverSignedEvent = { delivered = it },
|
|
74
|
+
logRejection = { rejection = it }
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return Result(forwarded, signed, delivered, rejection)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private fun validRequest(): String {
|
|
81
|
+
val event = JSONObject()
|
|
82
|
+
.put("kind", 27235)
|
|
83
|
+
.put("created_at", 1789552517L)
|
|
84
|
+
.put("tags", JSONArray())
|
|
85
|
+
.put("content", "")
|
|
86
|
+
.toString()
|
|
87
|
+
return "nostrsigner:${encode(event)}" +
|
|
88
|
+
"?type=sign_event&returnType=event&compressionType=none" +
|
|
89
|
+
"&callbackUrl=${encode(callback)}"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private fun encode(value: String): String {
|
|
93
|
+
return value.toByteArray(Charsets.UTF_8).joinToString("") { byte ->
|
|
94
|
+
val valueInt = byte.toInt() and 0xff
|
|
95
|
+
val character = valueInt.toChar()
|
|
96
|
+
if (character.isLetterOrDigit() || character in "-._~") {
|
|
97
|
+
character.toString()
|
|
98
|
+
} else {
|
|
99
|
+
"%${valueInt.toString(16).uppercase().padStart(2, '0')}"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private data class Result(
|
|
105
|
+
val forwarded: Boolean,
|
|
106
|
+
val signed: Boolean,
|
|
107
|
+
val delivered: String?,
|
|
108
|
+
val rejection: String?
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
private companion object {
|
|
112
|
+
const val SIGNED_EVENT =
|
|
113
|
+
"{\"id\":\"signed-id\",\"pubkey\":\"signed-pubkey\",\"sig\":\"signed-sig\"}"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class Nip55SignEventRequestTest {
|
|
9
|
+
private val callback =
|
|
10
|
+
"pakstr-7f7e720bd582301afabb6b441f89a4fb8d1a481c3e0286c69345d03e7cb1929c://nip55-callback#nip55_event="
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun parses_exact_regress_request() {
|
|
14
|
+
val rawUrl =
|
|
15
|
+
"nostrsigner:%7B%22kind%22%3A27235%2C%22created_at%22%3A1789552517%2C%22content%22%3A%22%22%2C%22tags%22%3A%5B%5B%22u%22%2C%22http%3A%2F%2F127.0.0.1%3A8080%2Fapi%2Fauth%2Flogin%22%5D%2C%5B%22method%22%2C%22POST%22%5D%5D%7D?compressionType=none&returnType=event&type=sign_event&callbackUrl=pakstr-7f7e720bd582301afabb6b441f89a4fb8d1a481c3e0286c69345d03e7cb1929c%3A%2F%2Fnip55-callback%23nip55_event%3D"
|
|
16
|
+
|
|
17
|
+
val parsed = Nip55SignEventParser.parse(rawUrl, callback)
|
|
18
|
+
|
|
19
|
+
assertTrue(parsed is Nip55SignEventRequest.Parsed)
|
|
20
|
+
parsed as Nip55SignEventRequest.Parsed
|
|
21
|
+
assertEquals(callback, parsed.callbackUrl)
|
|
22
|
+
assertEquals(27235, parsed.event.getInt("kind"))
|
|
23
|
+
assertEquals(1789552517L, parsed.event.getLong("created_at"))
|
|
24
|
+
assertEquals("", parsed.event.getString("content"))
|
|
25
|
+
assertEquals(
|
|
26
|
+
"http://127.0.0.1:8080/api/auth/login",
|
|
27
|
+
parsed.event.getJSONArray("tags").getJSONArray(0).getString(1)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun accepts_event_return_with_none_or_omitted_compression() {
|
|
33
|
+
val base = requestQuery("type=sign_event&returnType=event&callbackUrl=${encode(callback)}")
|
|
34
|
+
val withNone = requestQuery(
|
|
35
|
+
"type=sign_event&returnType=event&compressionType=none&callbackUrl=${encode(callback)}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assertTrue(Nip55SignEventParser.parse(base, callback) is Nip55SignEventRequest.Parsed)
|
|
39
|
+
assertTrue(Nip55SignEventParser.parse(withNone, callback) is Nip55SignEventRequest.Parsed)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun rejects_malformed_or_unsupported_sign_event() {
|
|
44
|
+
val unsupported = requestQuery(
|
|
45
|
+
"type=sign_event&returnType=event&compressionType=gzip&callbackUrl=${encode(callback)}"
|
|
46
|
+
)
|
|
47
|
+
val malformed =
|
|
48
|
+
"nostrsigner:%7Bbad?type=sign_event&returnType=event&callbackUrl=${encode(callback)}"
|
|
49
|
+
|
|
50
|
+
assertTrue(
|
|
51
|
+
Nip55SignEventParser.parse(unsupported, callback) is Nip55SignEventRequest.Rejected
|
|
52
|
+
)
|
|
53
|
+
assertTrue(
|
|
54
|
+
Nip55SignEventParser.parse(malformed, callback) is Nip55SignEventRequest.Rejected
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
fun rejects_foreign_callback() {
|
|
60
|
+
val rawUrl = requestQuery(
|
|
61
|
+
"type=sign_event&returnType=event&callbackUrl=${encode("other://nip55-callback#nip55_event=")}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assertTrue(
|
|
65
|
+
Nip55SignEventParser.parse(rawUrl, callback) is Nip55SignEventRequest.Rejected
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fun requestQuery(query: String): String {
|
|
70
|
+
val event = JSONObject()
|
|
71
|
+
.put("kind", 27235)
|
|
72
|
+
.put("created_at", 1789552517L)
|
|
73
|
+
.put("tags", org.json.JSONArray())
|
|
74
|
+
.put("content", "")
|
|
75
|
+
.toString()
|
|
76
|
+
return "nostrsigner:${encode(event)}?$query"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun encode(value: String): String {
|
|
80
|
+
return value.toByteArray(Charsets.UTF_8).joinToString("") { byte ->
|
|
81
|
+
val valueInt = byte.toInt() and 0xff
|
|
82
|
+
val character = valueInt.toChar()
|
|
83
|
+
if (character.isLetterOrDigit() || character in "-._~") {
|
|
84
|
+
character.toString()
|
|
85
|
+
} else {
|
|
86
|
+
"%${valueInt.toString(16).uppercase().padStart(2, '0')}"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class PakstrBridgeRuntimeApiBaseTest {
|
|
9
|
+
@Test
|
|
10
|
+
fun setterPersistsValidatedOverrideAndGetterReturnsEffectiveValue() {
|
|
11
|
+
val preferences = FakePreferences()
|
|
12
|
+
val bridge = PakstrBridge(
|
|
13
|
+
RuntimeApiBaseSettings.createForTesting(preferences) {
|
|
14
|
+
"https://default.example.com"
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assertTrue(bridge.setApiBaseUrl("https://override.example.com/"))
|
|
19
|
+
assertEquals("https://override.example.com", bridge.getApiBaseUrl())
|
|
20
|
+
assertFalse(bridge.setApiBaseUrl("http://unsafe.example.com"))
|
|
21
|
+
assertEquals("https://override.example.com", bridge.getApiBaseUrl())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun setterReportsPersistenceFailure() {
|
|
26
|
+
val bridge = PakstrBridge(
|
|
27
|
+
RuntimeApiBaseSettings.createForTesting(FakePreferences(commitResult = false)) {
|
|
28
|
+
null
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assertFalse(bridge.setApiBaseUrl("https://api.example.com"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private class FakePreferences(
|
|
36
|
+
private val commitResult: Boolean = true
|
|
37
|
+
) : RuntimeApiBaseSettings.Preferences {
|
|
38
|
+
private val values = mutableMapOf<String, String>()
|
|
39
|
+
|
|
40
|
+
override fun getString(key: String): String? = values[key]
|
|
41
|
+
|
|
42
|
+
override fun putString(key: String, value: String): Boolean {
|
|
43
|
+
if (commitResult) values[key] = value
|
|
44
|
+
return commitResult
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun remove(key: String): Boolean {
|
|
48
|
+
if (commitResult) values.remove(key)
|
|
49
|
+
return commitResult
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class RuntimeApiBaseProxyTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun proxyUsesUpdatedEffectiveValueOnSubsequentRequest() {
|
|
10
|
+
val preferences = FakePreferences()
|
|
11
|
+
val settings = RuntimeApiBaseSettings.createForTesting(preferences) {
|
|
12
|
+
"https://default.example.com"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
val firstRequestApiBase = settings.current().url!!
|
|
16
|
+
assertEquals(
|
|
17
|
+
"https://default.example.com/api/items",
|
|
18
|
+
proxyApiUrl(firstRequestApiBase, "/api/items").toString()
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assertTrue(settings.setOverride("https://updated.example.com/base/"))
|
|
22
|
+
|
|
23
|
+
val secondRequestApiBase = settings.current().url!!
|
|
24
|
+
assertEquals(
|
|
25
|
+
"https://updated.example.com/base/api/items",
|
|
26
|
+
proxyApiUrl(secondRequestApiBase, "/api/items").toString()
|
|
27
|
+
)
|
|
28
|
+
assertEquals(
|
|
29
|
+
"https://default.example.com/api/items",
|
|
30
|
+
proxyApiUrl(firstRequestApiBase, "/api/items").toString()
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private class FakePreferences : RuntimeApiBaseSettings.Preferences {
|
|
35
|
+
private val values = mutableMapOf<String, String>()
|
|
36
|
+
|
|
37
|
+
override fun getString(key: String): String? = values[key]
|
|
38
|
+
|
|
39
|
+
override fun putString(key: String, value: String): Boolean {
|
|
40
|
+
values[key] = value
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun remove(key: String): Boolean {
|
|
45
|
+
values.remove(key)
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class RuntimeApiBaseSettingsTest {
|
|
10
|
+
@Test
|
|
11
|
+
fun persistedOverrideTakesPrecedenceOverPackagedDefault() {
|
|
12
|
+
val preferences = FakePreferences()
|
|
13
|
+
val settings = settings(preferences, "https://default.example.com")
|
|
14
|
+
|
|
15
|
+
assertTrue(settings.setOverride("https://override.example.com/"))
|
|
16
|
+
assertEquals(
|
|
17
|
+
"https://override.example.com",
|
|
18
|
+
preferences.values[RuntimeApiBaseSettings.API_BASE_OVERRIDE_KEY]
|
|
19
|
+
)
|
|
20
|
+
assertEquals(
|
|
21
|
+
RuntimeApiBaseSettings.Value(
|
|
22
|
+
"https://override.example.com",
|
|
23
|
+
RuntimeApiBaseSettings.Source.RUNTIME_OVERRIDE
|
|
24
|
+
),
|
|
25
|
+
settings.current()
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun resetRemovesOverrideAndRestoresPackagedDefault() {
|
|
31
|
+
val preferences = FakePreferences()
|
|
32
|
+
val settings = settings(preferences, "https://default.example.com/")
|
|
33
|
+
settings.setOverride("https://override.example.com")
|
|
34
|
+
|
|
35
|
+
assertTrue(settings.reset())
|
|
36
|
+
assertFalse(preferences.values.containsKey(RuntimeApiBaseSettings.API_BASE_OVERRIDE_KEY))
|
|
37
|
+
assertEquals(
|
|
38
|
+
RuntimeApiBaseSettings.Value(
|
|
39
|
+
"https://default.example.com",
|
|
40
|
+
RuntimeApiBaseSettings.Source.PACKAGED_DEFAULT
|
|
41
|
+
),
|
|
42
|
+
settings.current()
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun noConfiguredDefaultDisablesApiProxy() {
|
|
48
|
+
val settings = settings(FakePreferences(), null)
|
|
49
|
+
|
|
50
|
+
assertEquals(
|
|
51
|
+
RuntimeApiBaseSettings.Value(
|
|
52
|
+
null,
|
|
53
|
+
RuntimeApiBaseSettings.Source.DISABLED
|
|
54
|
+
),
|
|
55
|
+
settings.current()
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun invalidPersistedValueIsIgnored() {
|
|
61
|
+
val preferences = FakePreferences(
|
|
62
|
+
mutableMapOf(
|
|
63
|
+
RuntimeApiBaseSettings.API_BASE_OVERRIDE_KEY to "http://unsafe.example.com"
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assertEquals(
|
|
68
|
+
"https://default.example.com",
|
|
69
|
+
settings(preferences, "https://default.example.com").current().url
|
|
70
|
+
)
|
|
71
|
+
assertNull(settings(preferences, null).current().url)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
fun invalidOverrideDoesNotPersist() {
|
|
76
|
+
val preferences = FakePreferences()
|
|
77
|
+
val settings = settings(preferences, "https://default.example.com")
|
|
78
|
+
|
|
79
|
+
assertFalse(settings.setOverride("https://user@example.com"))
|
|
80
|
+
assertTrue(preferences.values.isEmpty())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun settings(
|
|
84
|
+
preferences: FakePreferences,
|
|
85
|
+
packagedDefault: String?
|
|
86
|
+
): RuntimeApiBaseSettings = RuntimeApiBaseSettings.createForTesting(
|
|
87
|
+
preferences
|
|
88
|
+
) {
|
|
89
|
+
packagedDefault
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private class FakePreferences(
|
|
93
|
+
val values: MutableMap<String, String> = mutableMapOf()
|
|
94
|
+
) : RuntimeApiBaseSettings.Preferences {
|
|
95
|
+
override fun getString(key: String): String? = values[key]
|
|
96
|
+
|
|
97
|
+
override fun putString(key: String, value: String): Boolean {
|
|
98
|
+
values[key] = value
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun remove(key: String): Boolean {
|
|
103
|
+
values.remove(key)
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class RuntimeApiBaseValidatorTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun acceptsAbsoluteHttpsUrlWithHostname() {
|
|
10
|
+
assertEquals(
|
|
11
|
+
"https://api.example.com/v1",
|
|
12
|
+
RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
13
|
+
"https://api.example.com/v1"
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun normalizesTrailingSlashesAndHostCase() {
|
|
20
|
+
assertEquals(
|
|
21
|
+
"https://api.example.com/v1",
|
|
22
|
+
RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
23
|
+
"HTTPS://API.Example.COM/v1///"
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
assertEquals(
|
|
27
|
+
"https://api.example.com",
|
|
28
|
+
RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
29
|
+
"https://api.example.com/"
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun rejectsInvalidOrUnsafeUrls() {
|
|
36
|
+
listOf(
|
|
37
|
+
"",
|
|
38
|
+
" https://api.example.com",
|
|
39
|
+
"http://api.example.com",
|
|
40
|
+
"api.example.com",
|
|
41
|
+
"https:///v1",
|
|
42
|
+
"https://user:password@api.example.com",
|
|
43
|
+
"https://api.example.com?v=1",
|
|
44
|
+
"https://api.example.com#fragment",
|
|
45
|
+
"https://api.example.com/${"a".repeat(RuntimeApiBaseValidator.MAX_LENGTH)}"
|
|
46
|
+
).forEach { value ->
|
|
47
|
+
assertNull(value, RuntimeApiBaseValidator.validateAndCanonicalize(value))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|