Skip to content

Commit 2868948

Browse files
authored
Update to plugin registry API v1 (#10)
Signed-off-by: jorgee <[email protected]>
1 parent d956da2 commit 2868948

File tree

4 files changed

+178
-9
lines changed

4 files changed

+178
-9
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ dependencies {
1818
implementation 'com.google.code.gson:gson:2.10.1'
1919
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
2020
implementation 'org.apache.httpcomponents:httpmime:4.5.14'
21-
21+
2222
testImplementation('org.spockframework:spock-core:2.3-groovy-3.0')
23+
testImplementation 'org.wiremock:wiremock:3.5.4'
2324
}
2425

2526
test {

src/main/groovy/io/nextflow/gradle/registry/RegistryClient.groovy

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.nextflow.gradle.registry
22

3-
import com.google.gson.Gson
43
import groovy.transform.CompileStatic
54
import groovy.util.logging.Slf4j
65
import org.apache.http.client.methods.CloseableHttpResponse
@@ -12,20 +11,20 @@ import org.apache.http.util.EntityUtils
1211
@Slf4j
1312
@CompileStatic
1413
class RegistryClient {
15-
private final Gson gson = new Gson()
16-
1714
private final URI url
1815
private final String authToken
1916

2017
RegistryClient(URI url, String authToken) {
18+
if (!authToken)
19+
throw new RegistryPublishException("Authentication token not specified - Provide a valid token in 'publishing.registry' configuration")
2120
this.url = !url.toString().endsWith("/")
2221
? URI.create(url.toString() + "/")
2322
: url
2423
this.authToken = authToken
2524
}
2625

2726
def publish(String id, String version, File file) {
28-
def req = new HttpPost(url.resolve("publish"))
27+
def req = new HttpPost(url.resolve("v1/plugins/publish"))
2928
req.addHeader("Authorization", "Bearer ${authToken}")
3029
req.setEntity(MultipartEntityBuilder.create()
3130
.addTextBody("id", id)
@@ -39,13 +38,13 @@ class RegistryClient {
3938
if (rep.statusLine.statusCode != 200) {
4039
throw new RegistryPublishException(getErrorMessage(rep))
4140
}
42-
} catch (ConnectException e) {
43-
throw new RuntimeException("Unable to connect to plugin repository: (${e.message})")
41+
} catch (ConnectException | UnknownHostException e) {
42+
throw new RegistryPublishException("Unable to connect to plugin repository: ${e.message}", e)
4443
}
4544
}
4645

4746
private String getErrorMessage(CloseableHttpResponse rep) {
48-
def message = "Failed to publish plugin to registry $url: HTTP Response:${rep.statusLine}"
47+
def message = "Failed to publish plugin to registry $url: HTTP Response: ${rep.statusLine}"
4948
if( rep.entity ) {
5049
final String entityStr = EntityUtils.toString(rep.entity)
5150
if (entityStr) {
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package io.nextflow.gradle.registry
22

3+
import org.gradle.api.GradleException
4+
35
/**
46
* Custom exception class for registry publish task
57
*/
6-
class RegistryPublishException extends Exception{
8+
class RegistryPublishException extends GradleException{
79
RegistryPublishException(String s) {
810
super(s)
911
}
12+
13+
RegistryPublishException(String string, Throwable e) {
14+
super(string,e)
15+
}
1016
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package io.nextflow.gradle.registry
2+
3+
import com.github.tomakehurst.wiremock.WireMockServer
4+
import spock.lang.Specification
5+
import spock.lang.TempDir
6+
7+
import java.nio.file.Path
8+
9+
import static com.github.tomakehurst.wiremock.client.WireMock.*
10+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
11+
12+
class RegistryClientTest extends Specification {
13+
14+
WireMockServer wireMockServer
15+
RegistryClient client
16+
@TempDir
17+
Path tempDir
18+
19+
def setup() {
20+
wireMockServer = new WireMockServer(wireMockConfig().port(0))
21+
wireMockServer.start()
22+
def baseUrl = "http://localhost:${wireMockServer.port()}"
23+
client = new RegistryClient(new URI(baseUrl), "test-token")
24+
}
25+
26+
def cleanup() {
27+
wireMockServer?.stop()
28+
}
29+
30+
def "should construct client with URL ending in slash"() {
31+
when:
32+
def client1 = new RegistryClient(new URI("http://example.com"), "token")
33+
def client2 = new RegistryClient(new URI("http://example.com/"), "token")
34+
35+
then:
36+
client1.url.toString() == "http://example.com/"
37+
client2.url.toString() == "http://example.com/"
38+
}
39+
40+
def "Should fail when no token provided"(){
41+
when:
42+
new RegistryClient(new URI("http://example.com"), null)
43+
then:
44+
def ex = thrown(RegistryPublishException)
45+
ex.message == "Authentication token not specified - Provide a valid token in 'publishing.registry' configuration"
46+
}
47+
48+
def "should successfully publish plugin"() {
49+
given:
50+
def pluginFile = tempDir.resolve("test-plugin.zip").toFile()
51+
pluginFile.text = "fake plugin content"
52+
53+
wireMockServer.stubFor(post(urlEqualTo("/v1/plugins/publish"))
54+
.withHeader("Authorization", equalTo("Bearer test-token"))
55+
.withRequestBody(containing("id"))
56+
.withRequestBody(containing("version"))
57+
.withRequestBody(containing("file"))
58+
.willReturn(aResponse()
59+
.withStatus(200)
60+
.withBody('{"status": "success"}')))
61+
62+
when:
63+
client.publish("test-plugin", "1.0.0", pluginFile)
64+
65+
then:
66+
noExceptionThrown()
67+
68+
and:
69+
wireMockServer.verify(postRequestedFor(urlEqualTo("/v1/plugins/publish"))
70+
.withHeader("Authorization", equalTo("Bearer test-token")))
71+
}
72+
73+
def "should throw RegistryPublishException on HTTP error without response body"() {
74+
given:
75+
def pluginFile = tempDir.resolve("test-plugin.zip").toFile()
76+
pluginFile.text = "fake plugin content"
77+
78+
wireMockServer.stubFor(post(urlEqualTo("/v1/plugins/publish"))
79+
.willReturn(aResponse()
80+
.withStatus(400)))
81+
82+
when:
83+
client.publish("test-plugin", "1.0.0", pluginFile)
84+
85+
then:
86+
def ex = thrown(RegistryPublishException)
87+
ex.message.contains("Failed to publish plugin to registry")
88+
ex.message.contains("HTTP Response: HTTP/1.1 400 Bad Request")
89+
}
90+
91+
def "should throw RegistryPublishException on HTTP error with response body"() {
92+
given:
93+
def pluginFile = tempDir.resolve("test-plugin.zip").toFile()
94+
pluginFile.text = "fake plugin content"
95+
96+
wireMockServer.stubFor(post(urlEqualTo("/v1/plugins/publish"))
97+
.willReturn(aResponse()
98+
.withStatus(422)
99+
.withBody('{"error": "Plugin validation failed"}')))
100+
101+
when:
102+
client.publish("test-plugin", "1.0.0", pluginFile)
103+
104+
then:
105+
def ex = thrown(RegistryPublishException)
106+
ex.message.contains("Failed to publish plugin to registry")
107+
ex.message.contains("HTTP Response: HTTP/1.1 422 Unprocessable Entity")
108+
ex.message.contains('{"error": "Plugin validation failed"}')
109+
}
110+
111+
def "should fail when connection error"() {
112+
given:
113+
def pluginFile = tempDir.resolve("test-plugin.zip").toFile()
114+
pluginFile.text = "fake plugin content"
115+
116+
// Stop the server to simulate connection error
117+
wireMockServer.stop()
118+
119+
when:
120+
client.publish("test-plugin", "1.0.0", pluginFile)
121+
122+
then:
123+
def ex = thrown(RegistryPublishException)
124+
ex.message.startsWith("Unable to connect to plugin repository: ")
125+
ex.message.contains("failed: Connection refused")
126+
}
127+
128+
def "should fail when unknown host"(){
129+
given:
130+
def clientNotfound = new RegistryClient(new URI("http://fake-host.fake-domain-blabla.com"), "token")
131+
def pluginFile = tempDir.resolve("test-plugin.zip").toFile()
132+
pluginFile.text = "fake plugin content"
133+
134+
when:
135+
clientNotfound.publish("test-plugin", "1.0.0", pluginFile)
136+
137+
then:
138+
def ex = thrown(RegistryPublishException)
139+
ex.message == "Unable to connect to plugin repository: fake-host.fake-domain-blabla.com: Name or service not known"
140+
}
141+
142+
def "should send correct multipart form data"() {
143+
given:
144+
def pluginFile = tempDir.resolve("test-plugin.zip").toFile()
145+
pluginFile.text = "fake plugin zip content"
146+
147+
wireMockServer.stubFor(post(urlEqualTo("/v1/plugins/publish"))
148+
.willReturn(aResponse().withStatus(200)))
149+
150+
when:
151+
client.publish("my-plugin", "2.1.0", pluginFile)
152+
153+
then:
154+
wireMockServer.verify(postRequestedFor(urlEqualTo("/v1/plugins/publish"))
155+
.withHeader("Authorization", equalTo("Bearer test-token"))
156+
.withRequestBody(containing("Content-Disposition: form-data; name=\"id\""))
157+
.withRequestBody(containing("my-plugin"))
158+
.withRequestBody(containing("Content-Disposition: form-data; name=\"version\""))
159+
.withRequestBody(containing("2.1.0"))
160+
.withRequestBody(containing("Content-Disposition: form-data; name=\"file\""))
161+
.withRequestBody(containing("fake plugin zip content")))
162+
}
163+
}

0 commit comments

Comments
 (0)