1const t = require('tap')
2const mockNpm = require('../../fixtures/mock-npm')
3
4const mockToken = async (t, { profile, getCredentialsByURI, readUserInfo, ...opts } = {}) => {
5  const mocks = {}
6
7  if (profile) {
8    mocks['npm-profile'] = profile
9  }
10
11  if (readUserInfo) {
12    mocks['{LIB}/utils/read-user-info.js'] = readUserInfo
13  }
14
15  const mock = await mockNpm(t, {
16    ...opts,
17    command: 'token',
18    mocks,
19  })
20
21  // XXX: replace with mock registry
22  if (getCredentialsByURI) {
23    mock.npm.config.getCredentialsByURI = getCredentialsByURI
24  }
25
26  return mock
27}
28
29t.test('completion', async t => {
30  const { token } = await mockToken(t)
31
32  const testComp = (argv, expect) => {
33    t.resolveMatch(token.completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' '))
34  }
35
36  testComp(['npm', 'token'], ['list', 'revoke', 'create'])
37  testComp(['npm', 'token', 'list'], [])
38  testComp(['npm', 'token', 'revoke'], [])
39  testComp(['npm', 'token', 'create'], [])
40
41  t.rejects(token.completion({ conf: { argv: { remain: ['npm', 'token', 'foobar'] } } }), {
42    message: 'foobar not recognize',
43  })
44})
45
46t.test('token foobar', async t => {
47  const { token } = await mockToken(t)
48
49  await t.rejects(token.exec(['foobar']), /foobar is not a recognized subcommand/)
50})
51
52t.test('token list', async t => {
53  const now = new Date().toISOString()
54  const tokens = [
55    {
56      key: 'abcd1234abcd1234',
57      token: 'efgh5678efgh5678',
58      cidr_whitelist: null,
59      readonly: false,
60      created: now,
61      updated: now,
62    },
63    {
64      key: 'abcd1256',
65      token: 'hgfe8765',
66      cidr_whitelist: ['192.168.1.1/32'],
67      readonly: true,
68      created: now,
69      updated: now,
70    },
71  ]
72
73  const { token, joinedOutput } = await mockToken(t, {
74    config: { registry: 'https://registry.npmjs.org', otp: '123456' },
75    getCredentialsByURI: uri => {
76      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
77      return { token: 'thisisnotarealtoken' }
78    },
79    profile: {
80      listTokens: conf => {
81        t.same(conf.auth, { token: 'thisisnotarealtoken', otp: '123456' })
82        return tokens
83      },
84    },
85  })
86
87  await token.exec([])
88
89  const lines = joinedOutput().split(/\r?\n/)
90  t.match(lines[3], ' abcd123 ', 'includes the trimmed key')
91  t.match(lines[3], ' efgh56… ', 'includes the trimmed token')
92  t.match(lines[3], ` ${now.slice(0, 10)} `, 'includes the trimmed creation timestamp')
93  t.match(lines[3], ' no ', 'includes the "no" string for readonly state')
94  t.match(lines[5], ' abcd125 ', 'includes the trimmed key')
95  t.match(lines[5], ' hgfe87… ', 'includes the trimmed token')
96  t.match(lines[5], ` ${now.slice(0, 10)} `, 'includes the trimmed creation timestamp')
97  t.match(lines[5], ' yes ', 'includes the "no" string for readonly state')
98  t.match(lines[5], ` ${tokens[1].cidr_whitelist.join(',')} `, 'includes the cidr whitelist')
99})
100
101t.test('token list json output', async t => {
102  const now = new Date().toISOString()
103  const tokens = [
104    {
105      key: 'abcd1234abcd1234',
106      token: 'efgh5678efgh5678',
107      cidr_whitelist: null,
108      readonly: false,
109      created: now,
110      updated: now,
111    },
112  ]
113
114  const { token, joinedOutput } = await mockToken(t, {
115    config: { registry: 'https://registry.npmjs.org', json: true },
116    getCredentialsByURI: uri => {
117      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
118      return { username: 'foo', password: 'bar' }
119    },
120    profile: {
121      listTokens: conf => {
122        t.same(
123          conf.auth,
124          { basic: { username: 'foo', password: 'bar' } },
125          'passes the correct auth'
126        )
127        return tokens
128      },
129    },
130
131  })
132
133  await token.exec(['list'])
134
135  const parsed = JSON.parse(joinedOutput())
136  t.match(parsed, tokens, 'prints the json parsed tokens')
137})
138
139t.test('token list parseable output', async t => {
140  const now = new Date().toISOString()
141  const tokens = [
142    {
143      key: 'abcd1234abcd1234',
144      token: 'efgh5678efgh5678',
145      cidr_whitelist: null,
146      readonly: false,
147      created: now,
148      updated: now,
149    },
150    {
151      key: 'efgh5678ijkl9101',
152      token: 'hgfe8765',
153      cidr_whitelist: ['192.168.1.1/32'],
154      readonly: true,
155      created: now,
156      updated: now,
157    },
158  ]
159
160  const { token, joinedOutput } = await mockToken(t, {
161    config: { registry: 'https://registry.npmjs.org', parseable: true },
162    getCredentialsByURI: uri => {
163      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
164      return { auth: Buffer.from('foo:bar').toString('base64') }
165    },
166    profile: {
167      listTokens: conf => {
168        t.same(
169          conf.auth,
170          { basic: { username: 'foo', password: 'bar' } },
171          'passes the correct auth'
172        )
173        return tokens
174      },
175    },
176  })
177
178  await token.exec(['list'])
179
180  const lines = joinedOutput().split(/\r?\n/)
181
182  t.equal(
183    lines[0],
184    ['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t'),
185    'prints header'
186  )
187
188  t.equal(
189    lines[1],
190    [tokens[0].key, tokens[0].token, tokens[0].created, tokens[0].readonly, ''].join('\t'),
191    'prints token info'
192  )
193
194  t.equal(
195    lines[2],
196    [
197      tokens[1].key,
198      tokens[1].token,
199      tokens[1].created,
200      tokens[1].readonly,
201      tokens[1].cidr_whitelist.join(','),
202    ].join('\t'),
203    'prints token info'
204  )
205})
206
207t.test('token revoke', async t => {
208  const { token, joinedOutput } = await mockToken(t, {
209    config: { registry: 'https://registry.npmjs.org' },
210    getCredentialsByURI: uri => {
211      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
212      return {}
213    },
214    profile: {
215      listTokens: conf => {
216        t.same(conf.auth, {}, 'passes the correct empty auth')
217        return Promise.resolve([{ key: 'abcd1234' }])
218      },
219      removeToken: key => {
220        t.equal(key, 'abcd1234', 'deletes the correct token')
221      },
222    },
223  })
224
225  await token.exec(['rm', 'abcd'])
226
227  t.equal(joinedOutput(), 'Removed 1 token')
228})
229
230t.test('token revoke multiple tokens', async t => {
231  const { token, joinedOutput } = await mockToken(t, {
232    config: { registry: 'https://registry.npmjs.org' },
233    getCredentialsByURI: uri => {
234      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
235      return { token: 'thisisnotarealtoken' }
236    },
237    profile: {
238      listTokens: () => Promise.resolve([{ key: 'abcd1234' }, { key: 'efgh5678' }]),
239      removeToken: key => {
240        // this will run twice
241        t.ok(['abcd1234', 'efgh5678'].includes(key), 'deletes the correct token')
242      },
243    },
244  })
245
246  await token.exec(['revoke', 'abcd', 'efgh'])
247
248  t.equal(joinedOutput(), 'Removed 2 tokens')
249})
250
251t.test('token revoke json output', async t => {
252  const { token, joinedOutput } = await mockToken(t, {
253    config: { registry: 'https://registry.npmjs.org', json: true },
254    getCredentialsByURI: uri => {
255      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
256      return { token: 'thisisnotarealtoken' }
257    },
258    profile: {
259      listTokens: () => Promise.resolve([{ key: 'abcd1234' }]),
260      removeToken: key => {
261        t.equal(key, 'abcd1234', 'deletes the correct token')
262      },
263    },
264
265  })
266
267  await token.exec(['delete', 'abcd'])
268
269  const parsed = JSON.parse(joinedOutput())
270  t.same(parsed, ['abcd1234'], 'logs the token as json')
271})
272
273t.test('token revoke parseable output', async t => {
274  const { token, joinedOutput } = await mockToken(t, {
275    config: { registry: 'https://registry.npmjs.org', parseable: true },
276    getCredentialsByURI: uri => {
277      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
278      return { token: 'thisisnotarealtoken' }
279    },
280    profile: {
281      listTokens: () => Promise.resolve([{ key: 'abcd1234' }]),
282      removeToken: key => {
283        t.equal(key, 'abcd1234', 'deletes the correct token')
284      },
285    },
286  })
287
288  await token.exec(['remove', 'abcd'])
289
290  t.equal(joinedOutput(), 'abcd1234', 'logs the token as a string')
291})
292
293t.test('token revoke by token', async t => {
294  const { token, joinedOutput } = await mockToken(t, {
295    config: { registry: 'https://registry.npmjs.org' },
296    getCredentialsByURI: uri => {
297      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
298      return { token: 'thisisnotarealtoken' }
299    },
300    profile: {
301      listTokens: () => Promise.resolve([{ key: 'abcd1234', token: 'efgh5678' }]),
302      removeToken: key => {
303        t.equal(key, 'efgh5678', 'passes through user input')
304      },
305    },
306  })
307
308  await token.exec(['rm', 'efgh5678'])
309  t.equal(joinedOutput(), 'Removed 1 token')
310})
311
312t.test('token revoke requires an id', async t => {
313  const { token } = await mockToken(t)
314
315  await t.rejects(token.exec(['rm']), /`<tokenKey>` argument is required/)
316})
317
318t.test('token revoke ambiguous id errors', async t => {
319  const { token } = await mockToken(t, {
320    config: { registry: 'https://registry.npmjs.org' },
321    getCredentialsByURI: uri => {
322      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
323      return { token: 'thisisnotarealtoken' }
324    },
325    profile: {
326      listTokens: () => Promise.resolve([{ key: 'abcd1234' }, { key: 'abcd5678' }]),
327    },
328  })
329
330  await t.rejects(token.exec(['rm', 'abcd']), /Token ID "abcd" was ambiguous/)
331})
332
333t.test('token revoke unknown id errors', async t => {
334  const { token } = await mockToken(t, {
335    config: { registry: 'https://registry.npmjs.org' },
336    getCredentialsByURI: uri => {
337      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
338      return { token: 'thisisnotarealtoken' }
339    },
340    profile: {
341      listTokens: () => Promise.resolve([{ key: 'abcd1234' }]),
342    },
343  })
344
345  await t.rejects(token.exec(['rm', 'efgh']), /Unknown token id or value "efgh"./)
346})
347
348t.test('token create', async t => {
349  const now = new Date().toISOString()
350  const password = 'thisisnotreallyapassword'
351
352  const { token, joinedOutput } = await mockToken(t, {
353    config: {
354      registry: 'https://registry.npmjs.org',
355      cidr: ['10.0.0.0/8', '192.168.1.0/24'],
356    },
357    getCredentialsByURI: uri => {
358      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
359      return { token: 'thisisnotarealtoken' }
360    },
361    readUserInfo: {
362      password: () => Promise.resolve(password),
363    },
364    profile: {
365      createToken: (pw, readonly, cidr) => {
366        t.equal(pw, password)
367        t.equal(readonly, false)
368        t.same(cidr, ['10.0.0.0/8', '192.168.1.0/24'], 'defaults to empty array')
369        return {
370          key: 'abcd1234',
371          token: 'efgh5678',
372          created: now,
373          updated: now,
374          readonly: false,
375          cidr_whitelist: [],
376        }
377      },
378    },
379
380  })
381
382  await token.exec(['create'])
383
384  const lines = joinedOutput().split(/\r?\n/)
385  t.match(lines[1], 'token')
386  t.match(lines[1], 'efgh5678', 'prints the whole token')
387  t.match(lines[3], 'created')
388  t.match(lines[3], now, 'prints the correct timestamp')
389  t.match(lines[5], 'readonly')
390  t.match(lines[5], 'false', 'prints the readonly flag')
391  t.match(lines[7], 'cidr_whitelist')
392})
393
394t.test('token create json output', async t => {
395  const now = new Date().toISOString()
396  const password = 'thisisnotreallyapassword'
397
398  const { token } = await mockToken(t, {
399    config: { registry: 'https://registry.npmjs.org', json: true },
400    getCredentialsByURI: uri => {
401      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
402      return { token: 'thisisnotarealtoken' }
403    },
404    readUserInfo: {
405      password: () => Promise.resolve(password),
406    },
407    profile: {
408      createToken: (pw, readonly, cidr) => {
409        t.equal(pw, password)
410        t.equal(readonly, false)
411        t.same(cidr, [], 'defaults to empty array')
412        return {
413          key: 'abcd1234',
414          token: 'efgh5678',
415          created: now,
416          updated: now,
417          readonly: false,
418          cidr_whitelist: [],
419        }
420      },
421    },
422    output: spec => {
423      t.type(spec, 'string', 'outputs a string')
424      const parsed = JSON.parse(spec)
425      t.same(
426        parsed,
427        { token: 'efgh5678', created: now, readonly: false, cidr_whitelist: [] },
428        'outputs the correct object'
429      )
430    },
431  })
432
433  await token.exec(['create'])
434})
435
436t.test('token create parseable output', async t => {
437  const now = new Date().toISOString()
438  const password = 'thisisnotreallyapassword'
439
440  const { token, joinedOutput } = await mockToken(t, {
441    config: { registry: 'https://registry.npmjs.org', parseable: true },
442    getCredentialsByURI: uri => {
443      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
444      return { token: 'thisisnotarealtoken' }
445    },
446    readUserInfo: {
447      password: () => Promise.resolve(password),
448    },
449    profile: {
450      createToken: (pw, readonly, cidr) => {
451        t.equal(pw, password)
452        t.equal(readonly, false)
453        t.same(cidr, [], 'defaults to empty array')
454        return {
455          key: 'abcd1234',
456          token: 'efgh5678',
457          created: now,
458          updated: now,
459          readonly: false,
460          cidr_whitelist: [],
461        }
462      },
463    },
464  })
465
466  await token.exec(['create'])
467
468  const spec = joinedOutput().split(/\r?\n/)
469
470  t.match(spec[0], 'token\tefgh5678', 'prints the token')
471  t.match(spec[1], `created\t${now}`, 'prints the created timestamp')
472  t.match(spec[2], 'readonly\tfalse', 'prints the readonly flag')
473  t.match(spec[3], 'cidr_whitelist\t', 'prints the cidr whitelist')
474})
475
476t.test('token create ipv6 cidr', async t => {
477  const password = 'thisisnotreallyapassword'
478
479  const { token } = await mockToken(t, {
480    config: { registry: 'https://registry.npmjs.org', cidr: '::1/128' },
481    getCredentialsByURI: uri => {
482      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
483      return { token: 'thisisnotarealtoken' }
484    },
485    readUserInfo: {
486      password: () => Promise.resolve(password),
487    },
488  })
489
490  await t.rejects(
491    token.exec(['create']),
492    {
493      code: 'EINVALIDCIDR',
494      message: /CIDR whitelist can only contain IPv4 addresses, ::1\/128 is IPv6/,
495    },
496    'returns correct error'
497  )
498})
499
500t.test('token create invalid cidr', async t => {
501  const password = 'thisisnotreallyapassword'
502
503  const { token } = await mockToken(t, {
504    config: { registry: 'https://registry.npmjs.org', cidr: 'apple/cider' },
505    getCredentialsByURI: uri => {
506      t.equal(uri, 'https://registry.npmjs.org/', 'requests correct registry')
507      return { token: 'thisisnotarealtoken' }
508    },
509    readUserInfo: {
510      password: () => Promise.resolve(password),
511    },
512  })
513
514  await t.rejects(
515    token.exec(['create']),
516    { code: 'EINVALIDCIDR', message: /CIDR whitelist contains invalid CIDR entry: apple\/cider/ },
517    'returns correct error'
518  )
519})
520