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