Skip to content

Commit 6438e4a

Browse files
committed
implement support for integration testing with quickstart
1 parent e60721e commit 6438e4a

File tree

6 files changed

+306
-1
lines changed

6 files changed

+306
-1
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ shadow-jar = "8.3.5"
1010

1111
# OpenZiti Edge API
1212
ziti-api = "0.26.39"
13+
ziti-cli = "1.3.3"
1314

1415
# third party
1516
lazysodium-java = "5.1.4"

ziti/build.gradle.kts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
import kotlinx.coroutines.delay
3+
import kotlinx.coroutines.runBlocking
24

35
/*
46
* Copyright (c) 2018-2021 NetFoundry Inc.
@@ -94,6 +96,92 @@ tasks.register<Jar>("dokkaJar") {
9496
from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
9597
}
9698

99+
@Suppress("UnstableApiUsage")
100+
testing {
101+
suites {
102+
val integrationTest by registering(JvmTestSuite::class) {
103+
dependencies {
104+
implementation(libs.kotlin.coroutines.test)
105+
implementation(libs.slf4j.simple)
106+
implementation(project(":management-api"))
107+
}
108+
}
109+
}
110+
}
111+
112+
kotlin {
113+
target {
114+
// make sure we can test internal components
115+
compilations.getByName("integrationTest").associateWith(compilations.getByName("main"))
116+
}
117+
}
118+
119+
val zitiVersion = libs.versions.ziti.cli.get()
120+
val binDir = layout.buildDirectory.dir("bin").get()
121+
val zitiCLI = binDir.file("ziti")
122+
val quickstartHome = layout.buildDirectory.dir("quickstart").get()
123+
124+
tasks.register<Exec>("buildZiti") {
125+
group = LifecycleBasePlugin.BUILD_GROUP
126+
description = "Builds the Ziti CLI"
127+
environment("GOBIN", binDir.asFile.absolutePath)
128+
commandLine("go", "install", "github.com/openziti/ziti/ziti@v${zitiVersion}")
129+
outputs.file(zitiCLI)
130+
}
131+
132+
tasks.register("start-quickstart") {
133+
description = "Starts Ziti quickstart"
134+
val qsLog = layout.buildDirectory.file("quickstart.log").get().asFile
135+
val errLog = layout.buildDirectory.file("error.log").get().asFile
136+
137+
doLast {
138+
val pb = ProcessBuilder().apply {
139+
command(
140+
zitiCLI.toString(),
141+
"edge", "quickstart", "--home", quickstartHome.asFile.absolutePath)
142+
redirectOutput(qsLog)
143+
redirectError(errLog)
144+
}
145+
val qsProc = pb.start()
146+
ext["quickstart"] = qsProc
147+
runBlocking {
148+
149+
while(true) {
150+
delay(1000)
151+
if (!qsProc.isAlive) {
152+
throw GradleException("quickstart failed: check ${errLog.absolutePath}")
153+
}
154+
155+
val started = kotlin.runCatching {
156+
qsLog.readLines().find { it.contains("controller and router started") }
157+
}
158+
if (started.getOrNull() != null) {
159+
logger.lifecycle("quickstart is ready")
160+
break
161+
}
162+
163+
logger.lifecycle("waiting for qs router...")
164+
}
165+
}
166+
}
167+
dependsOn("buildZiti")
168+
}
169+
170+
tasks.register("stop-quickstart") {
171+
doLast {
172+
val proc = ext["quickstart"] as Process?
173+
proc?.let {
174+
logger.lifecycle("stopping quickstart...")
175+
it.destroy()
176+
}
177+
}
178+
}
179+
180+
tasks.named("integrationTest") {
181+
dependsOn("start-quickstart")
182+
finalizedBy("stop-quickstart")
183+
}
184+
97185
publishing {
98186
publications {
99187
create<MavenPublication>(project.name) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2018-2025 NetFoundry Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.openziti.api
18+
19+
import kotlinx.coroutines.test.runTest
20+
import org.junit.jupiter.api.BeforeAll
21+
import org.junit.jupiter.api.Test
22+
import org.openziti.integ.ManagementHelper
23+
import org.openziti.integ.ManagementHelper.enrollmentApi
24+
import org.openziti.integ.ManagementHelper.identityApi
25+
import org.openziti.integ.ManagementHelper.sslContext
26+
import org.openziti.management.model.EnrollmentCreate
27+
import org.openziti.management.model.IdentityCreate
28+
import org.openziti.management.model.IdentityType
29+
import java.net.URL
30+
import java.time.OffsetDateTime
31+
32+
33+
class ControllerTests {
34+
35+
companion object {
36+
@JvmStatic
37+
@BeforeAll
38+
fun init() {
39+
val identity = identityApi.createIdentity(
40+
IdentityCreate().name("test-${System.nanoTime()}").isAdmin(false).type(IdentityType.DEVICE)
41+
).get().data!!
42+
43+
val exp = OffsetDateTime.now().plusDays(1)
44+
val enrollReq = EnrollmentCreate()
45+
.identityId(identity.id!!)
46+
.method(EnrollmentCreate.MethodEnum.OTT)
47+
.expiresAt(exp)
48+
val enrollment = enrollmentApi.createEnrollment(enrollReq).get().data
49+
val token = identityApi.getIdentityEnrollments(identity.id).get().data.first().jwt
50+
println(token)
51+
}
52+
53+
}
54+
55+
@Test
56+
fun testCreateController() = runTest {
57+
58+
// assertThrows<Controller.NotAvailableException> {
59+
// Controller.getActiveController(listOf(), sslContext)
60+
// }
61+
//
62+
// assertThrows<Controller.NotAvailableException> {
63+
// Controller.getActiveController(listOf(URL("https://google.com")), sslContext)
64+
// }
65+
//
66+
// val ctrl = assertDoesNotThrow {
67+
// Controller.getActiveController(listOf(
68+
// URL("https://google.com"),
69+
// URL("https://localhost:1280")
70+
// ), sslContext)
71+
// }
72+
//
73+
// Assertions.assertEquals("/edge/client/v1", ctrl.endpoint.path)
74+
75+
val ctrl = Controller(URL(ManagementHelper.api.baseUri), sslContext)
76+
ctrl.version()
77+
}
78+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright (c) 2018-2025 NetFoundry Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.openziti.integ
18+
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.asExecutor
21+
import org.openziti.management.ApiClient
22+
import org.openziti.management.api.AuthenticationApi
23+
import org.openziti.management.api.EnrollmentApi
24+
import org.openziti.management.api.IdentityApi
25+
import org.openziti.management.api.InformationalApi
26+
import org.openziti.management.model.Authenticate
27+
import org.openziti.util.parsePKCS7
28+
import java.net.InetAddress
29+
import java.net.URI
30+
import java.net.http.HttpClient
31+
import java.net.http.HttpRequest
32+
import java.net.http.HttpResponse
33+
import java.security.KeyStore
34+
import java.security.SecureRandom
35+
import java.security.cert.X509Certificate
36+
import javax.net.ssl.SSLContext
37+
import javax.net.ssl.TrustManagerFactory
38+
import javax.net.ssl.X509TrustManager
39+
40+
internal object ManagementHelper {
41+
42+
private val defaultURL = "https://${InetAddress.getLocalHost().hostName}:1280"
43+
44+
private object DefaultCredentials: Authenticate() {
45+
init {
46+
username = "admin"
47+
password = "admin"
48+
}
49+
}
50+
51+
private val trustAllssl: SSLContext = SSLContext.getInstance("TLS").apply {
52+
init(null, arrayOf(TrustAll), SecureRandom())
53+
}
54+
55+
56+
internal val sslContext: SSLContext by lazy {
57+
val clt = HttpClient.newBuilder().sslContext(trustAllssl).build()
58+
val req = HttpRequest.newBuilder(URI.create("$defaultURL/.well-known/est/cacerts")).build()
59+
val pkcs7 = clt.send(req, HttpResponse.BodyHandlers.ofByteArray()).body()
60+
parsePKCS7(pkcs7).makeSSL()
61+
}
62+
63+
internal val identityApi by lazy { IdentityApi(api) }
64+
internal val enrollmentApi by lazy { EnrollmentApi(api) }
65+
66+
private object TrustAll : X509TrustManager {
67+
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
68+
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
69+
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
70+
}
71+
72+
private fun List<X509Certificate>.makeSSL(): SSLContext {
73+
val ks = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
74+
load(null, null)
75+
this@makeSSL.forEach {
76+
setCertificateEntry(it.subjectX500Principal.name, it)
77+
}
78+
}
79+
80+
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
81+
init(ks)
82+
}
83+
84+
return SSLContext.getInstance("TLS").apply {
85+
init(null, tmf.trustManagers, SecureRandom())
86+
}
87+
}
88+
89+
90+
internal val api: ApiClient by lazy {
91+
ApiClient().apply {
92+
updateBaseUri(defaultURL)
93+
setHttpClientBuilder(
94+
HttpClient.newBuilder()
95+
.sslContext(sslContext)
96+
.executor(Dispatchers.IO.asExecutor())
97+
)
98+
99+
val mgmtUrl = InformationalApi(this@apply)
100+
.listVersion().get()
101+
.data.apiVersions?.get("edge-management")?.get("v1")?.path!!
102+
setBasePath(mgmtUrl)
103+
104+
val token = AuthenticationApi(this@apply)
105+
.authenticate("password", DefaultCredentials)
106+
.get().data.token
107+
setRequestInterceptor { it.header("zt-session", token) }
108+
}
109+
}
110+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# Copyright (c) 2018-2022 NetFoundry Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
org.slf4j.simpleLogger.defaultLogLevel=trace
18+
org.slf4j.simpleLogger.log.tlschannel=warn
19+
org.slf4j.simpleLogger.showDateTime=true
20+
#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z
21+

ziti/src/main/kotlin/org/openziti/util/Certs.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ internal class PrivateKeySigner(val key: PrivateKey, val sigAlg: String) : Conte
117117
override fun getSignature(): ByteArray = sig.sign()
118118
}
119119

120+
internal fun parsePKCS7(bundle: ByteArray) =
121+
Base64.getMimeDecoder().decode(bundle).run {
122+
CertificateFactory.getInstance("X.509")
123+
.generateCertificates(inputStream())
124+
.filterIsInstance<X509Certificate>()
125+
}
126+
120127
internal fun getCACerts(api: URI, serverKey: Key): Collection<X509Certificate> {
121128
val con = api.resolve("/.well-known/est/cacerts").toURL().openConnection() as HttpsURLConnection
122129
con.setRequestProperty("Accept", "application/pkcs7-mime")
@@ -133,7 +140,7 @@ internal fun getCACerts(api: URI, serverKey: Key): Collection<X509Certificate> {
133140
val bytes = Base64.getMimeDecoder().decode(b)
134141
val cf = CertificateFactory.getInstance("X.509")
135142

136-
return cf.generateCertificates(bytes.inputStream()) as Collection<X509Certificate>
143+
return cf.generateCertificates(bytes.inputStream()).filterIsInstance<X509Certificate>()
137144
}
138145
}
139146

0 commit comments

Comments
 (0)