• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const fs = require('fs')
4const path = require('path')
5
6const test = require('tap').test
7const Tacks = require('tacks')
8const Dir = Tacks.Dir
9const File = Tacks.File
10const common = require('../common-tap.js')
11const isWindows = require('../../lib/utils/is-windows.js')
12
13const base = common.pkg
14const noFunding = path.join(base, 'no-funding-package')
15const maintainerOwnsAllDeps = path.join(base, 'maintainer-owns-all-deps')
16const nestedNoFundingPackages = path.join(base, 'nested-no-funding-packages')
17const nestedMultipleFundingPackages = path.join(base, 'nested-multiple-funding-packages')
18const fundingStringShorthand = path.join(base, 'funding-string-shorthand')
19
20function getFixturePackage ({ name, version, dependencies, funding }, extras) {
21  const getDeps = () => Object
22    .keys(dependencies)
23    .reduce((res, dep) => (Object.assign({}, res, {
24      [dep]: '*'
25    })), {})
26
27  return Dir(Object.assign({
28    'package.json': File({
29      name,
30      version: version || '1.0.0',
31      funding: (funding === undefined) ? {
32        type: 'individual',
33        url: 'http://example.com/donate'
34      } : funding,
35      dependencies: dependencies && getDeps(dependencies)
36    })
37  }, extras))
38}
39
40const fixture = new Tacks(Dir({
41  'funding-string-shorthand': Dir({
42    'package.json': File({
43      name: 'funding-string-shorthand',
44      version: '0.0.0',
45      funding: 'https://example.com/sponsor'
46    })
47  }),
48  'no-funding-package': Dir({
49    'package.json': File({
50      name: 'no-funding-package',
51      version: '0.0.0'
52    })
53  }),
54  'maintainer-owns-all-deps': getFixturePackage({
55    name: 'maintainer-owns-all-deps',
56    dependencies: {
57      'dep-foo': '*',
58      'dep-bar': '*'
59    }
60  }, {
61    node_modules: Dir({
62      'dep-foo': getFixturePackage({
63        name: 'dep-foo',
64        dependencies: {
65          'dep-sub-foo': '*'
66        }
67      }, {
68        node_modules: Dir({
69          'dep-sub-foo': getFixturePackage({
70            name: 'dep-sub-foo'
71          })
72        })
73      }),
74      'dep-bar': getFixturePackage({
75        name: 'dep-bar'
76      })
77    })
78  }),
79  'nested-no-funding-packages': getFixturePackage({
80    name: 'nested-no-funding-packages',
81    funding: null,
82    dependencies: {
83      foo: '*'
84    },
85    devDependencies: {
86      lorem: '*'
87    }
88  }, {
89    node_modules: Dir({
90      foo: getFixturePackage({
91        name: 'foo',
92        dependencies: {
93          bar: '*'
94        },
95        funding: null
96      }, {
97        node_modules: Dir({
98          bar: getFixturePackage({
99            name: 'bar'
100          }, {
101            node_modules: Dir({
102              'sub-bar': getFixturePackage({
103                name: 'sub-bar',
104                funding: 'https://example.com/sponsor'
105              })
106            })
107          })
108        })
109      }),
110      lorem: getFixturePackage({
111        name: 'lorem',
112        funding: {
113          url: 'https://example.com/lorem'
114        }
115      })
116    })
117  }),
118  'nested-multiple-funding-packages': getFixturePackage({
119    name: 'nested-multiple-funding-packages',
120    funding: [
121      'https://one.example.com',
122      'https://two.example.com'
123    ],
124    dependencies: {
125      foo: '*'
126    },
127    devDependencies: {
128      bar: '*'
129    }
130  }, {
131    node_modules: Dir({
132      foo: getFixturePackage({
133        name: 'foo',
134        funding: [
135          'http://example.com',
136          { url: 'http://sponsors.example.com/me' },
137          'http://collective.example.com'
138        ]
139      }),
140      bar: getFixturePackage({
141        name: 'bar',
142        funding: [
143          'http://collective.example.com',
144          { url: 'http://sponsors.example.com/you' }
145        ]
146      })
147    })
148  })
149}))
150
151function checkOutput (t, { code, stdout, stderr }) {
152  t.is(code, 0, `exited code 0`)
153  t.is(stderr, '', 'no warnings')
154}
155
156function jsonTest (t, { assertionMsg, expected, stdout }) {
157  let parsed = JSON.parse(stdout)
158  t.deepEqual(parsed, expected, assertionMsg)
159}
160
161function snapshotTest (t, { stdout, assertionMsg }) {
162  t.matchSnapshot(stdout, assertionMsg)
163}
164
165function testFundCmd ({ title, assertionMsg, args = [], opts = {}, output = checkOutput, assertion = snapshotTest, expected }) {
166  const validate = (t) => (err, code, stdout, stderr) => {
167    if (err) throw err
168
169    output(t, { code, stdout, stderr })
170    assertion(t, { assertionMsg, expected, stdout })
171  }
172
173  return test(title, (t) => {
174    t.plan(3)
175    common.npm(['fund', '--unicode=false'].concat(args), opts, validate(t))
176  })
177}
178
179test('setup', function (t) {
180  fixture.remove(base)
181  fixture.create(base)
182  t.end()
183})
184
185testFundCmd({
186  title: 'fund with no package containing funding',
187  assertionMsg: 'should print empty funding info',
188  opts: { cwd: noFunding }
189})
190
191testFundCmd({
192  title: 'fund in which same maintainer owns all its deps',
193  assertionMsg: 'should print stack packages together',
194  opts: { cwd: maintainerOwnsAllDeps }
195})
196
197testFundCmd({
198  title: 'fund in which same maintainer owns all its deps, using --json option',
199  assertionMsg: 'should print stack packages together',
200  args: ['--json'],
201  opts: { cwd: maintainerOwnsAllDeps },
202  assertion: jsonTest,
203  expected: {
204    length: 3,
205    name: 'maintainer-owns-all-deps',
206    version: '1.0.0',
207    funding: { type: 'individual', url: 'http://example.com/donate' },
208    dependencies: {
209      'dep-bar': {
210        version: '1.0.0',
211        funding: { type: 'individual', url: 'http://example.com/donate' }
212      },
213      'dep-foo': {
214        version: '1.0.0',
215        funding: { type: 'individual', url: 'http://example.com/donate' },
216        dependencies: {
217          'dep-sub-foo': {
218            version: '1.0.0',
219            funding: { type: 'individual', url: 'http://example.com/donate' }
220          }
221        }
222      }
223    }
224  }
225})
226
227testFundCmd({
228  title: 'fund containing multi-level nested deps with no funding',
229  assertionMsg: 'should omit dependencies with no funding declared',
230  opts: { cwd: nestedNoFundingPackages }
231})
232
233testFundCmd({
234  title: 'fund containing multi-level nested deps with no funding, using --json option',
235  assertionMsg: 'should omit dependencies with no funding declared',
236  args: ['--json'],
237  opts: { cwd: nestedNoFundingPackages },
238  assertion: jsonTest,
239  expected: {
240    length: 3,
241    name: 'nested-no-funding-packages',
242    version: '1.0.0',
243    dependencies: {
244      lorem: { version: '1.0.0', funding: { url: 'https://example.com/lorem' } },
245      bar: {
246        version: '1.0.0',
247        funding: { type: 'individual', url: 'http://example.com/donate' },
248        dependencies: {
249          'sub-bar': {
250            version: '1.0.0',
251            funding: { url: 'https://example.com/sponsor' }
252          }
253        }
254      }
255    }
256  }
257})
258
259testFundCmd({
260  title: 'fund containing multi-level nested deps with multiple funding sources, using --json option',
261  assertionMsg: 'should omit dependencies with no funding declared',
262  args: ['--json'],
263  opts: { cwd: nestedMultipleFundingPackages },
264  assertion: jsonTest,
265  expected: {
266    length: 2,
267    name: 'nested-multiple-funding-packages',
268    version: '1.0.0',
269    funding: [
270      {
271        url: 'https://one.example.com'
272      },
273      {
274        url: 'https://two.example.com'
275      }
276    ],
277    dependencies: {
278      bar: {
279        version: '1.0.0',
280        funding: [
281          {
282            url: 'http://collective.example.com'
283          },
284          {
285            url: 'http://sponsors.example.com/you'
286          }
287        ]
288      },
289      foo: {
290        version: '1.0.0',
291        funding: [
292          {
293            url: 'http://example.com'
294          },
295          {
296            url: 'http://sponsors.example.com/me'
297          },
298          {
299            url: 'http://collective.example.com'
300          }
301        ]
302      }
303    }
304  }
305})
306
307testFundCmd({
308  title: 'fund does not support global',
309  assertionMsg: 'should throw EFUNDGLOBAL error',
310  args: ['--global'],
311  output: (t, { code, stdout, stderr }) => {
312    t.is(code, 1, `exited code 0`)
313    const [ errCode, errCmd ] = stderr.split('\n')
314    t.matchSnapshot(`${errCode}\n${errCmd}`, 'should write error msgs to stderr')
315  }
316})
317
318testFundCmd({
319  title: 'fund does not support global, using --json option',
320  assertionMsg: 'should throw EFUNDGLOBAL error',
321  args: ['--global', '--json'],
322  output: (t, { code, stdout, stderr }) => {
323    t.is(code, 1, `exited code 0`)
324    const [ errCode, errCmd ] = stderr.split('\n')
325    t.matchSnapshot(`${errCode}\n${errCmd}`, 'should write error msgs to stderr')
326  },
327  assertion: jsonTest,
328  expected: {
329    error: {
330      code: 'EFUNDGLOBAL',
331      summary: '`npm fund` does not support global packages',
332      detail: ''
333    }
334  }
335})
336
337testFundCmd({
338  title: 'fund using package argument with no browser',
339  assertionMsg: 'should open funding url',
340  args: ['.', '--no-browser'],
341  opts: { cwd: maintainerOwnsAllDeps }
342})
343
344testFundCmd({
345  title: 'fund using string shorthand',
346  assertionMsg: 'should open string-only url',
347  args: ['.', '--no-browser'],
348  opts: { cwd: fundingStringShorthand }
349})
350
351testFundCmd({
352  title: 'fund using nested packages with multiple sources',
353  assertionMsg: 'should prompt with all available URLs',
354  args: ['.'],
355  opts: { cwd: nestedMultipleFundingPackages }
356})
357
358testFundCmd({
359  title: 'fund using nested packages with multiple sources, with a source number',
360  assertionMsg: 'should open the numbered URL',
361  args: ['.', '--which=1', '--no-browser'],
362  opts: { cwd: nestedMultipleFundingPackages }
363})
364
365testFundCmd({
366  title: 'fund using package argument with no browser, using --json option',
367  assertionMsg: 'should open funding url',
368  args: ['.', '--json', '--no-browser'],
369  opts: { cwd: maintainerOwnsAllDeps },
370  assertion: jsonTest,
371  expected: {
372    title: 'individual funding available at the following URL',
373    url: 'http://example.com/donate'
374  }
375})
376
377if (!isWindows) {
378  test('fund using package argument', function (t) {
379    const fakeBrowser = path.join(common.pkg, '_script.sh')
380    const outFile = path.join(common.pkg, '_output')
381
382    const s = '#!/usr/bin/env bash\n' +
383            'echo "$@" > ' + JSON.stringify(common.pkg) + '/_output\n'
384    fs.writeFileSync(fakeBrowser, s)
385    fs.chmodSync(fakeBrowser, '0755')
386
387    common.npm([
388      'fund', '.',
389      '--loglevel=silent',
390      '--browser=' + fakeBrowser
391    ], { cwd: maintainerOwnsAllDeps }, function (err, code, stdout, stderr) {
392      t.ifError(err, 'repo command ran without error')
393      t.equal(code, 0, 'exit ok')
394      var res = fs.readFileSync(outFile, 'utf8')
395      t.equal(res, 'http://example.com/donate\n')
396      t.end()
397    })
398  })
399}
400
401test('cleanup', function (t) {
402  t.pass(base)
403  fixture.remove(base)
404  t.end()
405})
406