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