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