Skip to content

Commit 4b74924

Browse files
committed
feat: Enhance CI workflow to run unit tests and add JUnit dependency for testing
1 parent 3146322 commit 4b74924

5 files changed

Lines changed: 146 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties
8888
export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}"
8989
./run init action gradle
90-
./gradlew app:assembleOssDebug
90+
./gradlew app:testOssDebugUnitTest app:assembleOssDebug
9191
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
9292
APK=$(dirname $APK)
9393
echo "APK=$APK" >> $GITHUB_ENV

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ dependencies {
7373
implementation("com.google.guava:guava:31.0.1-android")
7474
implementation("org.ini4j:ini4j:0.5.4")
7575

76+
testImplementation("junit:junit:4.13.2")
77+
7678
implementation("com.simplecityapps:recyclerview-fastscroll:2.0.1") {
7779
exclude(group = "androidx.recyclerview")
7880
exclude(group = "androidx.appcompat")

app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.nekohasekai.sagernet.fmt.wireguard
22

33
import moe.matsuri.nb4a.SingBoxOptions
4-
import moe.matsuri.nb4a.utils.Util
54
import moe.matsuri.nb4a.utils.listByLineOrComma
65

6+
private const val BASE64_ALPHABET =
7+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
8+
79
fun genReserved(anyStr: String): String {
810
val values = anyStr
911
.trim()
@@ -12,20 +14,26 @@ fun genReserved(anyStr: String): String {
1214
.filter { it.isNotEmpty() }
1315
.map { value -> value.toIntOrNull()?.takeIf { it in 0..255 } ?: return anyStr }
1416
if (values.size != 3) return anyStr
15-
return Util.b64EncodeOneLine(ByteArray(3) { values[it].toByte() })
17+
val bits = (values[0] shl 16) or (values[1] shl 8) or values[2]
18+
return buildString(4) {
19+
append(BASE64_ALPHABET[(bits ushr 18) and 0x3F])
20+
append(BASE64_ALPHABET[(bits ushr 12) and 0x3F])
21+
append(BASE64_ALPHABET[(bits ushr 6) and 0x3F])
22+
append(BASE64_ALPHABET[bits and 0x3F])
23+
}
1624
}
1725

1826
fun buildSingBoxEndpointWireGuardBean(bean: WireGuardBean): SingBoxOptions.Endpoint_WireGuardOptions {
1927
return SingBoxOptions.Endpoint_WireGuardOptions().apply {
2028
type = "wireguard"
2129
address = bean.localAddress.listByLineOrComma()
2230
private_key = bean.privateKey
23-
mtu = bean.mtu
31+
mtu = bean.mtu?.takeIf { it > 0 }
2432
listen_port = bean.listenPort?.takeIf { it > 0 }
2533
peers = listOf(
2634
SingBoxOptions.Endpoint_WireGuardPeer().apply {
27-
address = bean.serverAddress
28-
port = bean.serverPort
35+
address = bean.serverAddress?.takeIf { it.isNotBlank() }
36+
port = bean.serverPort?.takeIf { it > 0 }
2937
public_key = bean.peerPublicKey
3038
pre_shared_key = bean.peerPreSharedKey.takeIf { it.isNotBlank() }
3139
allowed_ips = listOf("0.0.0.0/0", "::/0")
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package io.nekohasekai.sagernet.fmt.wireguard
2+
3+
import com.esotericsoftware.kryo.io.ByteBufferOutput
4+
import io.nekohasekai.sagernet.fmt.KryoConverters
5+
import moe.matsuri.nb4a.utils.JavaUtil
6+
import org.junit.Assert.assertEquals
7+
import org.junit.Assert.assertFalse
8+
import org.junit.Assert.assertTrue
9+
import org.junit.Test
10+
import java.io.ByteArrayOutputStream
11+
12+
class WireGuardFmtTest {
13+
14+
@Test
15+
fun buildEndpointMapsCompleteFieldsAndDualStackAllowedIps() {
16+
val endpoint = buildSingBoxEndpointWireGuardBean(completeBean("[0, 1, 2]"))
17+
18+
assertEquals("wireguard", endpoint.type)
19+
assertEquals(listOf("10.0.0.2/32", "fd00::2/128"), endpoint.address)
20+
assertTrue(TEST_PRIVATE_KEY == endpoint.private_key)
21+
assertEquals(1380, endpoint.mtu)
22+
assertEquals(51821, endpoint.listen_port)
23+
24+
assertEquals(1, endpoint.peers.size)
25+
val peer = endpoint.peers.single()
26+
assertEquals("198.51.100.10", peer.address)
27+
assertEquals(51820, peer.port)
28+
assertEquals(TEST_PUBLIC_KEY, peer.public_key)
29+
assertTrue(TEST_PRE_SHARED_KEY == peer.pre_shared_key)
30+
assertEquals(listOf("0.0.0.0/0", "::/0"), peer.allowed_ips)
31+
assertEquals(25, peer.persistent_keepalive_interval)
32+
assertEquals("AAEC", peer.reserved)
33+
}
34+
35+
@Test
36+
fun buildEndpointOmitsOptionalZeroAndBlankFieldsFromJson() {
37+
val bean = completeBean("").apply {
38+
serverAddress = ""
39+
serverPort = 0
40+
peerPreSharedKey = ""
41+
mtu = 0
42+
listenPort = 0
43+
persistentKeepaliveInterval = 0
44+
}
45+
46+
val endpointJson = JavaUtil.gson.toJsonTree(buildSingBoxEndpointWireGuardBean(bean)).asJsonObject
47+
assertFalse(endpointJson.has("mtu"))
48+
assertFalse(endpointJson.has("listen_port"))
49+
50+
val peerJson = endpointJson.getAsJsonArray("peers").single().asJsonObject
51+
assertFalse(peerJson.has("address"))
52+
assertFalse(peerJson.has("port"))
53+
assertFalse(peerJson.has("pre_shared_key"))
54+
assertFalse(peerJson.has("persistent_keepalive_interval"))
55+
assertFalse(peerJson.has("reserved"))
56+
}
57+
58+
@Test
59+
fun genReservedConvertsThreeByteListFormsToBase64() {
60+
assertEquals("AAEC", genReserved("[0, 1, 2]"))
61+
assertEquals("AAEC", genReserved("0,\n1 2"))
62+
}
63+
64+
@Test
65+
fun genReservedPreservesExistingBase64() {
66+
assertEquals("AAEC", genReserved("AAEC"))
67+
}
68+
69+
@Test
70+
fun wireGuardBeanDeserializesVersionTwoWithNewFieldsDefaulted() {
71+
val bean = KryoConverters.deserialize(WireGuardBean(), versionTwoFixture())
72+
73+
assertEquals("198.51.100.10", bean.serverAddress)
74+
assertEquals(51820, bean.serverPort)
75+
assertEquals("10.0.0.2/32", bean.localAddress)
76+
assertTrue(TEST_PRIVATE_KEY == bean.privateKey)
77+
assertEquals(TEST_PUBLIC_KEY, bean.peerPublicKey)
78+
assertTrue(TEST_PRE_SHARED_KEY == bean.peerPreSharedKey)
79+
assertEquals(1380, bean.mtu)
80+
assertEquals("AAEC", bean.reserved)
81+
assertEquals(0, bean.listenPort)
82+
assertEquals(0, bean.persistentKeepaliveInterval)
83+
}
84+
85+
private fun completeBean(reservedValue: String) = WireGuardBean().apply {
86+
serverAddress = "198.51.100.10"
87+
serverPort = 51820
88+
localAddress = "10.0.0.2/32, fd00::2/128"
89+
privateKey = TEST_PRIVATE_KEY
90+
peerPublicKey = TEST_PUBLIC_KEY
91+
peerPreSharedKey = TEST_PRE_SHARED_KEY
92+
mtu = 1380
93+
reserved = reservedValue
94+
listenPort = 51821
95+
persistentKeepaliveInterval = 25
96+
}
97+
98+
private fun versionTwoFixture(): ByteArray {
99+
val bytes = ByteArrayOutputStream()
100+
val output = ByteBufferOutput(bytes)
101+
102+
// WireGuardBean v2 payload. This deliberately does not call the current serializer.
103+
output.writeInt(2)
104+
output.writeString("198.51.100.10")
105+
output.writeInt(51820)
106+
output.writeString("10.0.0.2/32")
107+
output.writeString(TEST_PRIVATE_KEY)
108+
output.writeString(TEST_PUBLIC_KEY)
109+
output.writeString(TEST_PRE_SHARED_KEY)
110+
output.writeInt(1380)
111+
output.writeString("AAEC")
112+
113+
// AbstractBean extra payload.
114+
output.writeInt(1)
115+
output.writeString("legacy-wireguard-test")
116+
output.writeString("")
117+
output.writeString("")
118+
output.flush()
119+
output.close()
120+
return bytes.toByteArray()
121+
}
122+
123+
private companion object {
124+
// Deliberately invalid-for-production, deterministic fixture material.
125+
const val TEST_PRIVATE_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
126+
const val TEST_PUBLIC_KEY = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
127+
const val TEST_PRE_SHARED_KEY = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC="
128+
}
129+
}

openspec/changes/migrate-wireguard-endpoint/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- [x] 1.1 对照 `nb4a.properties` 的 sing-box v1.13.16 官方源码/JSON schema,记录 WireGuard endpoint、peer、根 `endpoints` 和 endpoint registry 的准确字段与类型;同时核对 T4A WireGuardBean 的 Kryo 版本,只有缺失字段时才采用追加字段的兼容升级。
44
- [x] 1.2 在 `SingBoxOptions.java` 增加根 endpoints、Endpoint 基类及 v1.13.16 WireGuard endpoint/peer options,并在 WireGuard 格式模块实现 endpoint builder、可选字段省略和 reserved 三字节列表/base64 兼容转换;保留 legacy outbound options 但移除产品 builder 对它的调用。
5-
- [ ] 1.3 增加 JVM 单元测试,覆盖完整字段、双栈 allowed_ips、零值/空值省略、reserved 两种表示以及 Bean 旧版本反序列化(如本批修改 Bean);fixture 必须使用无生产价值的测试密钥且失败输出不得泄露完整私钥/PSK。
5+
- [x] 1.3 增加 JVM 单元测试,覆盖完整字段、双栈 allowed_ips、零值/空值省略、reserved 两种表示以及 Bean 旧版本反序列化(如本批修改 Bean);fixture 必须使用无生产价值的测试密钥且失败输出不得泄露完整私钥/PSK。
66
- [ ] 1.4 提交本批至 GitHub Actions `CI / Build OSS APK`(其依赖 `CI / Native Build (LibCore)`);预期两个 job 均成功且目标 WireGuard 单测通过,回传 Actions run URL、失败/成功测试摘要和 APK 编译成功记录。收到该证据前不得开始第 2 批。
77

88
## 2. 单节点 endpoint 配置拓扑

0 commit comments

Comments
 (0)