1
+ /*
2
+ * Copyright 2013-2025, Seqera Labs
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
+ * http://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 nextflow.cli
18
+
19
+ import nextflow.exception.AbortOperationException
20
+ import org.junit.Rule
21
+ import spock.lang.Specification
22
+ import spock.lang.TempDir
23
+ import test.OutputCapture
24
+
25
+ import java.nio.file.Files
26
+ import java.nio.file.Path
27
+
28
+ /**
29
+ * Test CmdAuth functionality
30
+ *
31
+ * @author Phil Ewels <[email protected] >
32
+ */
33
+ class CmdAuthTest extends Specification {
34
+
35
+ @Rule
36
+ OutputCapture capture = new OutputCapture ()
37
+
38
+ @TempDir
39
+ Path tempDir
40
+
41
+ def ' should have correct name' () {
42
+ given :
43
+ def cmd = new CmdAuth ()
44
+
45
+ expect :
46
+ cmd. getName() == ' auth'
47
+ }
48
+
49
+ def ' should define correct constants' () {
50
+ expect :
51
+ CmdAuth . SEQERA_ENDPOINTS . prod == ' https://api.cloud.seqera.io'
52
+ CmdAuth . SEQERA_ENDPOINTS . stage == ' https://api.cloud.stage-seqera.io'
53
+ CmdAuth . SEQERA_ENDPOINTS . dev == ' https://api.cloud.dev-seqera.io'
54
+ CmdAuth . API_TIMEOUT_MS == 10000
55
+ CmdAuth . AUTH_POLL_TIMEOUT_RETRIES == 60
56
+ CmdAuth . AUTH_POLL_INTERVAL_SECONDS == 5
57
+ CmdAuth . WORKSPACE_SELECTION_THRESHOLD == 8
58
+ }
59
+
60
+ def ' should show usage when no args provided' () {
61
+ given :
62
+ def cmd = new CmdAuth ()
63
+
64
+ when :
65
+ cmd. run()
66
+
67
+ then :
68
+ def output = capture. toString()
69
+ output. contains(' Manage Seqera Platform authentication' )
70
+ output. contains(' Usage: nextflow auth <sub-command> [options]' )
71
+ output. contains(' Commands:' )
72
+ output. contains(' login' )
73
+ output. contains(' logout' )
74
+ output. contains(' config' )
75
+ output. contains(' status' )
76
+ }
77
+
78
+ def ' should show specific command usage' () {
79
+ given :
80
+ def cmd = new CmdAuth ()
81
+ cmd. args = [' login' ]
82
+
83
+ when :
84
+ cmd. usage()
85
+
86
+ then :
87
+ def output = capture. toString()
88
+ output. contains(' Authenticate with Seqera Platform' )
89
+ output. contains(' Usage: nextflow auth login' )
90
+ output. contains(' -u, -url <endpoint>' )
91
+ }
92
+
93
+ def ' should throw error for unknown command' () {
94
+ given :
95
+ def cmd = new CmdAuth ()
96
+ cmd. args = [' unknown' ]
97
+
98
+ when :
99
+ cmd. run()
100
+
101
+ then :
102
+ def ex = thrown(AbortOperationException )
103
+ ex. message. contains(' Unknown auth sub-command: unknown' )
104
+ }
105
+
106
+ def ' should suggest closest command for typos' () {
107
+ given :
108
+ def cmd = new CmdAuth ()
109
+
110
+ when :
111
+ cmd. getCmd([' loginn' ])
112
+
113
+ then :
114
+ def ex = thrown(AbortOperationException )
115
+ ex. message. contains(' Unknown auth sub-command: loginn' )
116
+ ex. message. contains(' Did you mean one of these?' )
117
+ ex. message. contains(' login' )
118
+ }
119
+
120
+ def ' should identify cloud endpoints correctly' () {
121
+ given :
122
+ def cmd = new CmdAuth ()
123
+
124
+ expect :
125
+ cmd. getCloudEndpointInfo(' https://api.cloud.seqera.io' ). isCloud == true
126
+ cmd. getCloudEndpointInfo(' https://api.cloud.seqera.io' ). environment == ' prod'
127
+ cmd. getCloudEndpointInfo(' https://api.cloud.stage-seqera.io' ). isCloud == true
128
+ cmd. getCloudEndpointInfo(' https://api.cloud.stage-seqera.io' ). environment == ' stage'
129
+ cmd. getCloudEndpointInfo(' https://api.cloud.dev-seqera.io' ). isCloud == true
130
+ cmd. getCloudEndpointInfo(' https://api.cloud.dev-seqera.io' ). environment == ' dev'
131
+ cmd. getCloudEndpointInfo(' https://cloud.seqera.io/api' ). isCloud == true
132
+ cmd. getCloudEndpointInfo(' https://cloud.seqera.io/api' ). environment == ' prod'
133
+ cmd. getCloudEndpointInfo(' https://enterprise.example.com' ). isCloud == false
134
+ cmd. getCloudEndpointInfo(' https://enterprise.example.com' ). environment == null
135
+ }
136
+
137
+ def ' should identify cloud endpoint from URL' () {
138
+ given :
139
+ def cmd = new CmdAuth ()
140
+
141
+ expect :
142
+ cmd. isCloudEndpoint(' https://api.cloud.seqera.io' ) == true
143
+ cmd. isCloudEndpoint(' https://api.cloud.stage-seqera.io' ) == true
144
+ cmd. isCloudEndpoint(' https://enterprise.example.com' ) == false
145
+ }
146
+
147
+ def ' should validate argument count correctly' () {
148
+ given :
149
+ def cmd = new CmdAuth ()
150
+
151
+ when :
152
+ cmd. validateArgumentCount([' extra' ], ' test' )
153
+
154
+ then :
155
+ def ex = thrown(AbortOperationException )
156
+ ex. message == ' Too many arguments for test command'
157
+
158
+ when :
159
+ cmd. validateArgumentCount([], ' test' )
160
+
161
+ then :
162
+ noExceptionThrown()
163
+ }
164
+
165
+ def ' should read config correctly' () {
166
+ given :
167
+ def cmd = new CmdAuth ()
168
+
169
+ expect :
170
+ // readConfig method should return a Map
171
+ cmd. readConfig() instanceof Map
172
+ }
173
+
174
+ def ' should clean tower config from existing content' () {
175
+ given :
176
+ def cmd = new CmdAuth ()
177
+ def content = '''
178
+ // Some other config
179
+ process {
180
+ executor = 'local'
181
+ }
182
+
183
+ // Seqera Platform configuration
184
+ tower {
185
+ accessToken = 'old-token'
186
+ enabled = true
187
+ }
188
+
189
+ tower.endpoint = 'old-endpoint'
190
+
191
+ // More config
192
+ params.test = true
193
+ '''
194
+
195
+ when :
196
+ def cleaned = cmd. cleanTowerConfig(content)
197
+
198
+ then :
199
+ ! cleaned. contains(' tower {' )
200
+ ! cleaned. contains(' accessToken = \' old-token\' ' )
201
+ ! cleaned. contains(' tower.endpoint' )
202
+ ! cleaned. contains(' Seqera Platform configuration' )
203
+ cleaned. contains(' process {' )
204
+ cleaned. contains(' params.test = true' )
205
+ }
206
+
207
+ def ' should handle config writing' () {
208
+ given :
209
+ def cmd = new CmdAuth ()
210
+ def config = [
211
+ ' tower.accessToken' : ' test-token' ,
212
+ ' tower.enabled' : true
213
+ ]
214
+
215
+ when :
216
+ cmd. writeConfig(config, null )
217
+
218
+ then :
219
+ noExceptionThrown()
220
+ }
221
+
222
+ def ' login command should validate too many arguments' () {
223
+ given :
224
+ def cmd = new CmdAuth ()
225
+ cmd. args = [' login' , ' extra' ]
226
+
227
+ when :
228
+ cmd. run()
229
+
230
+ then :
231
+ def ex = thrown(AbortOperationException )
232
+ ex. message. contains(' Too many arguments for login command' )
233
+ }
234
+
235
+ def ' logout command should validate too many arguments' () {
236
+ given :
237
+ def cmd = new CmdAuth ()
238
+ cmd. args = [' logout' , ' extra' ]
239
+
240
+ when :
241
+ cmd. run()
242
+
243
+ then :
244
+ def ex = thrown(AbortOperationException )
245
+ ex. message. contains(' Too many arguments for logout command' )
246
+ }
247
+
248
+ def ' config command should validate too many arguments' () {
249
+ given :
250
+ def cmd = new CmdAuth ()
251
+ cmd. args = [' config' , ' extra' ]
252
+
253
+ when :
254
+ cmd. run()
255
+
256
+ then :
257
+ def ex = thrown(AbortOperationException )
258
+ ex. message. contains(' Too many arguments for config command' )
259
+ }
260
+
261
+ def ' status command should validate too many arguments' () {
262
+ given :
263
+ def cmd = new CmdAuth ()
264
+ cmd. args = [' status' , ' extra' ]
265
+
266
+ when :
267
+ cmd. run()
268
+
269
+ then :
270
+ def ex = thrown(AbortOperationException )
271
+ ex. message. contains(' Too many arguments for status command' )
272
+ }
273
+
274
+ def ' login command should use provided API URL' () {
275
+ given :
276
+ def cmd = new CmdAuth ()
277
+ cmd. args = [' login' ]
278
+ cmd. apiUrl = ' https://api.example.com'
279
+
280
+ when :
281
+ def loginCmd = cmd. getCmd(cmd. args)
282
+
283
+ then :
284
+ loginCmd instanceof CmdAuth.LoginCmd
285
+
286
+ when :
287
+ cmd. run()
288
+
289
+ then :
290
+ loginCmd. apiUrl == ' https://api.example.com'
291
+ }
292
+
293
+ def ' should have all required subcommands' () {
294
+ given :
295
+ def cmd = new CmdAuth ()
296
+
297
+ expect :
298
+ cmd. commands. size() == 4
299
+ cmd. commands. find { it. name == ' login' } != null
300
+ cmd. commands. find { it. name == ' logout' } != null
301
+ cmd. commands. find { it. name == ' config' } != null
302
+ cmd. commands. find { it. name == ' status' } != null
303
+ }
304
+
305
+ def ' should create HTTP connection with correct properties' () {
306
+ given :
307
+ def cmd = new CmdAuth ()
308
+
309
+ when :
310
+ def connection = cmd. createHttpConnection(' https://example.com' , ' GET' , ' test-token' )
311
+
312
+ then :
313
+ connection. requestMethod == ' GET'
314
+ connection. connectTimeout == CmdAuth . API_TIMEOUT_MS
315
+ connection. readTimeout == CmdAuth . API_TIMEOUT_MS
316
+
317
+ when :
318
+ def connectionNoAuth = cmd. createHttpConnection(' https://example.com' , ' POST' )
319
+
320
+ then :
321
+ connectionNoAuth. requestMethod == ' POST'
322
+ connectionNoAuth. connectTimeout == CmdAuth . API_TIMEOUT_MS
323
+ connectionNoAuth. readTimeout == CmdAuth . API_TIMEOUT_MS
324
+ }
325
+ }
0 commit comments