diff --git a/lib/fund.js b/lib/fund.js index fdd1bd07ab3f5..9b5f10999b1b6 100644 --- a/lib/fund.js +++ b/lib/fund.js @@ -13,6 +13,7 @@ const { } = require('libnpmfund') const npm = require('./npm.js') +const completion = require('./utils/completion/installed-deep.js') const output = require('./utils/output.js') const openUrl = require('./utils/open-url.js') const usageUtil = require('./utils/usage.js') @@ -23,16 +24,6 @@ const usage = usageUtil( 'npm fund [--json] [--browser] [--unicode] [[<@scope>/] [--which=]' ) -const completion = (opts, cb) => { - const argv = opts.conf.argv.remain - switch (argv[2]) { - case 'fund': - return cb(null, []) - default: - return cb(new Error(argv[2] + ' not recognized')) - } -} - const cmd = (args, cb) => fund(args).then(() => cb()).catch(cb) function printJSON (fundingInfo) { @@ -126,14 +117,8 @@ async function openFundingUrl ({ path, tree, spec, fundingSourceNumber }) { } } - let { funding } = retrievePackageMetadata() || {} - - if (!funding) { - // if still has not funding info, let's try - // fetching metadata from the registry then - const manifest = await pacote.manifest(arg, npm.flatOptions) - funding = manifest.funding - } + const { funding } = retrievePackageMetadata() || + await pacote.manifest(arg, npm.flatOptions).catch(() => ({})) const validSources = [] .concat(normalizeFunding(funding)) diff --git a/tap-snapshots/test-tap-fund.js-TAP.test.js b/tap-snapshots/test-lib-fund.js-TAP.test.js similarity index 55% rename from tap-snapshots/test-tap-fund.js-TAP.test.js rename to tap-snapshots/test-lib-fund.js-TAP.test.js index a616f0fc0f496..d21dd5741343f 100644 --- a/tap-snapshots/test-tap-fund.js-TAP.test.js +++ b/tap-snapshots/test-lib-fund.js-TAP.test.js @@ -5,7 +5,14 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' -exports[`test/tap/fund.js TAP fund containing multi-level nested deps with no funding > should omit dependencies with no funding declared 1`] = ` +exports[`test/lib/fund.js TAP fund a package with type and multiple sources > should print prompt select message 1`] = ` +1: Foo funding available at the following URL: http://example.com/foo +2: Lorem funding available at the following URL: http://example.com/foo-lorem +Run \`npm fund [<@scope>/] --which=1\`, for example, to open the first funding URL listed in that package + +` + +exports[`test/lib/fund.js TAP fund containing multi-level nested deps with no funding > should omit dependencies with no funding declared 1`] = ` nested-no-funding-packages@1.0.0 +-- https://example.com/lorem | \`-- lorem@1.0.0 @@ -15,49 +22,48 @@ nested-no-funding-packages@1.0.0 ` -exports[`test/tap/fund.js TAP fund in which same maintainer owns all its deps > should print stack packages together 1`] = ` +exports[`test/lib/fund.js TAP fund in which same maintainer owns all its deps > should print stack packages together 1`] = ` http://example.com/donate \`-- maintainer-owns-all-deps@1.0.0, dep-foo@1.0.0, dep-sub-foo@1.0.0, dep-bar@1.0.0 ` -exports[`test/tap/fund.js TAP fund using nested packages with multiple sources > should prompt with all available URLs 1`] = ` +exports[`test/lib/fund.js TAP fund pkg missing version number > should print name only 1`] = ` +http://example.com/foo + \`-- foo + + +` + +exports[`test/lib/fund.js TAP fund using nested packages with multiple sources > should prompt with all available URLs 1`] = ` 1: Funding available at the following URL: https://one.example.com 2: Funding available at the following URL: https://two.example.com Run \`npm fund [<@scope>/] --which=1\`, for example, to open the first funding URL listed in that package ` -exports[`test/tap/fund.js TAP fund using nested packages with multiple sources, with a source number > should open the numbered URL 1`] = ` +exports[`test/lib/fund.js TAP fund using nested packages with multiple sources, with a source number > should open the numbered URL 1`] = ` Funding available at the following URL: - -https://one.example.com - + https://one.example.com ` -exports[`test/tap/fund.js TAP fund using package argument with no browser > should open funding url 1`] = ` +exports[`test/lib/fund.js TAP fund using package argument > should open funding url 1`] = ` individual funding available at the following URL: - -http://example.com/donate - + http://example.com/donate ` -exports[`test/tap/fund.js TAP fund using pkg name while having conflicting versions > should open greatest version 1`] = ` +exports[`test/lib/fund.js TAP fund using pkg name while having conflicting versions > should open greatest version 1`] = ` Funding available at the following URL: - -http://example.com/2 - + http://example.com/2 ` -exports[`test/tap/fund.js TAP fund using string shorthand > should open string-only url 1`] = ` +exports[`test/lib/fund.js TAP fund using string shorthand > should open string-only url 1`] = ` Funding available at the following URL: - -https://example.com/sponsor - + https://example.com/sponsor ` -exports[`test/tap/fund.js TAP fund with no package containing funding > should print empty funding info 1`] = ` +exports[`test/lib/fund.js TAP fund with no package containing funding > should print empty funding info 1`] = ` no-funding-package@0.0.0 diff --git a/test/lib/fund.js b/test/lib/fund.js new file mode 100644 index 0000000000000..cd9ac97ffb2b9 --- /dev/null +++ b/test/lib/fund.js @@ -0,0 +1,729 @@ +'use strict' + +const { test } = require('tap') +const requireInject = require('require-inject') + +const version = '1.0.0' +const funding = { + type: 'individual', + url: 'http://example.com/donate' +} + +const maintainerOwnsAllDeps = { + 'package.json': JSON.stringify({ + name: 'maintainer-owns-all-deps', + version, + funding, + dependencies: { + 'dep-foo': '*', + 'dep-bar': '*' + } + }), + node_modules: { + 'dep-foo': { + 'package.json': JSON.stringify({ + name: 'dep-foo', + version, + funding, + dependencies: { + 'dep-sub-foo': '*' + } + }), + node_modules: { + 'dep-sub-foo': { + 'package.json': JSON.stringify({ + name: 'dep-sub-foo', + version, + funding + }) + } + } + }, + 'dep-bar': { + 'package.json': JSON.stringify({ + name: 'dep-bar', + version, + funding + }) + } + } +} + +const nestedNoFundingPackages = { + 'package.json': JSON.stringify({ + name: 'nested-no-funding-packages', + version, + dependencies: { + foo: '*' + }, + devDependencies: { + lorem: '*' + } + }), + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version, + dependencies: { + bar: '*' + } + }), + node_modules: { + bar: { + 'package.json': JSON.stringify({ + name: 'bar', + version, + funding + }), + node_modules: { + 'sub-bar': { + 'package.json': JSON.stringify({ + name: 'sub-bar', + version, + funding: 'https://example.com/sponsor' + }) + } + } + } + } + }, + lorem: { + 'package.json': JSON.stringify({ + name: 'lorem', + version, + funding: { + url: 'https://example.com/lorem' + } + }) + } + } +} + +const nestedMultipleFundingPackages = { + 'package.json': JSON.stringify({ + name: 'nested-multiple-funding-packages', + version, + funding: [ + 'https://one.example.com', + 'https://two.example.com' + ], + dependencies: { + foo: '*' + }, + devDependencies: { + bar: '*' + } + }), + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version, + funding: [ + 'http://example.com', + { url: 'http://sponsors.example.com/me' }, + 'http://collective.example.com' + ] + }) + }, + bar: { + 'package.json': JSON.stringify({ + name: 'bar', + version, + funding: [ + 'http://collective.example.com', + { url: 'http://sponsors.example.com/you' } + ] + }) + } + } +} + +const conflictingFundingPackages = { + 'package.json': JSON.stringify({ + name: 'conflicting-funding-packages', + version, + dependencies: { + foo: '1.0.0' + }, + devDependencies: { + bar: '1.0.0' + } + }), + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version: '1.0.0', + funding: 'http://example.com/1' + }) + }, + bar: { + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version: '2.0.0', + funding: 'http://example.com/2' + }) + } + }, + 'package.json': JSON.stringify({ + name: 'bar', + version: '1.0.0', + dependencies: { + foo: '2.0.0' + } + }) + } + } +} + +let result = '' +let printUrl = '' +const _flatOptions = { + json: false, + global: false, + prefix: undefined, + unicode: false, + which: undefined +} +let openUrl = (url, msg, cb) => { + if (url === 'http://npmjs.org') { + cb(new Error('ERROR')) + return + } + if (_flatOptions.json) { + printUrl = JSON.stringify({ + title: msg, + url: url + }) + } else { + printUrl = `${msg}:\n ${url}` + } + cb() +} +const fund = requireInject('../../lib/fund.js', { + '../../lib/npm.js': { + flatOptions: _flatOptions, + get prefix () { return _flatOptions.prefix } + }, + '../../lib/utils/open-url.js': openUrl, + '../../lib/utils/output.js': msg => { result += msg + '\n' }, + 'pacote': { + manifest: (arg) => arg.name === 'ntl' + ? Promise.resolve({ + funding: 'http://example.com/pacote' + }) + : Promise.reject(new Error('ERROR')) + } +}) + + +test('fund with no package containing funding', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'no-funding-package', + version: '0.0.0' + }) + }) + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(result, 'should print empty funding info') + result = '' + t.end() + }) +}) + +test('fund in which same maintainer owns all its deps', t => { + _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(result, 'should print stack packages together') + result = '' + t.end() + }) +}) + +test('fund in which same maintainer owns all its deps, using --json option', t => { + _flatOptions.json = true + _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.deepEqual( + JSON.parse(result), + { + length: 3, + name: 'maintainer-owns-all-deps', + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' }, + dependencies: { + 'dep-bar': { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' } + }, + 'dep-foo': { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' }, + dependencies: { + 'dep-sub-foo': { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' } + } + } + } + } + }, + 'should print stack packages together' + ) + + result = '' + _flatOptions.json = false + t.end() + }) +}) + +test('fund containing multi-level nested deps with no funding', t => { + _flatOptions.prefix = t.testdir(nestedNoFundingPackages) + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot( + result, + 'should omit dependencies with no funding declared' + ) + + result = '' + t.end() + }) +}) + +test('fund containing multi-level nested deps with no funding, using --json option', t => { + _flatOptions.prefix = t.testdir(nestedNoFundingPackages) + _flatOptions.json = true + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.deepEqual( + JSON.parse(result), + { + length: 2, + name: 'nested-no-funding-packages', + version: '1.0.0', + dependencies: { + lorem: { + version: '1.0.0', + funding: { url: 'https://example.com/lorem' } + }, + bar: { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' } + } + } + }, + 'should omit dependencies with no funding declared in json output' + ) + + result = '' + _flatOptions.json = false + t.end() + }) +}) + +test('fund containing multi-level nested deps with no funding, using --json option', t => { + _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) + _flatOptions.json = true + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.deepEqual( + JSON.parse(result), + { + length: 2, + name: 'nested-multiple-funding-packages', + version: '1.0.0', + funding: [ + { + url: 'https://one.example.com' + }, + { + url: 'https://two.example.com' + } + ], + dependencies: { + bar: { + version: '1.0.0', + funding: [ + { + url: 'http://collective.example.com' + }, + { + url: 'http://sponsors.example.com/you' + } + ] + }, + foo: { + version: '1.0.0', + funding: [ + { + url: 'http://example.com' + }, + { + url: 'http://sponsors.example.com/me' + }, + { + url: 'http://collective.example.com' + } + ] + } + } + }, + 'should list multiple funding entries in json output' + ) + + result = '' + _flatOptions.json = false + t.end() + }) +}) + +test('fund does not support global', t => { + _flatOptions.prefix = t.testdir({}) + _flatOptions.global = true + + fund([], (err) => { + t.match(err.code, 'EFUNDGLOBAL', 'should throw EFUNDGLOBAL error') + + result = '' + _flatOptions.global = false + t.end() + }) +}) + +test('fund using package argument', t => { + _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) + + fund(['.'], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(printUrl, 'should open funding url') + + printUrl = '' + t.end() + }) +}) + +test('fund does not support global, using --json option', t => { + _flatOptions.prefix = t.testdir({}) + _flatOptions.global = true + _flatOptions.json = true + + fund([], (err) => { + t.equal(err.code, 'EFUNDGLOBAL', 'should use EFUNDGLOBAL error code') + t.equal( + err.message, + '`npm fund` does not support global packages', + 'should use expected error msg' + ) + + _flatOptions.global = false + _flatOptions.json = false + t.end() + }) +}) + +test('fund using string shorthand', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'funding-string-shorthand', + version: '0.0.0', + funding: 'https://example.com/sponsor' + }) + }) + + fund(['.'], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(printUrl, 'should open string-only url') + + printUrl = '' + t.end() + }) +}) + +test('fund using nested packages with multiple sources', t => { + _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) + + fund(['.'], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(result, 'should prompt with all available URLs') + + result = '' + t.end() + }) +}) + +test('fund using symlink ref', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'using-symlink-ref', + version: '1.0.0' + }), + 'a': { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + funding: 'http://example.com/a' + }) + }, + node_modules: { + a: t.fixture('symlink', '../a') + } + }) + + // using symlinked ref + fund(['./node_modules/a'], (err) => { + t.ifError(err, 'should not error out') + t.match( + printUrl, + 'http://example.com/a', + 'should retrieve funding url from symlink' + ) + + printUrl = '' + + // using target ref + fund(['./a'], (err) => { + t.match( + printUrl, + 'http://example.com/a', + 'should retrieve funding url from symlink target' + ) + + printUrl = '' + result = '' + t.end() + }) + }) +}) + +test('fund using data from actual tree', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'using-actual-tree', + version: '1.0.0' + }), + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + funding: 'http://example.com/a' + }) + }, + b: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + funding: 'http://example.com/b' + }), + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.1', + funding: 'http://example.com/_AAA' + }) + } + } + } + } + }) + + // using symlinked ref + fund(['a'], (err) => { + t.ifError(err, 'should not error out') + t.match( + printUrl, + 'http://example.com/_AAA', + 'should retrieve fund info from actual tree, using greatest version found' + ) + + printUrl = '' + t.end() + }) +}) + +test('fund using nested packages with multiple sources, with a source number', t => { + _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) + _flatOptions.which = '1' + + fund(['.'], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(printUrl, 'should open the numbered URL') + + _flatOptions.which = undefined + printUrl = '' + t.end() + }) +}) + +test('fund using pkg name while having conflicting versions', t => { + _flatOptions.prefix = t.testdir(conflictingFundingPackages) + _flatOptions.which = '1' + + fund(['foo'], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(printUrl, 'should open greatest version') + + printUrl = '' + t.end() + }) +}) + +test('fund using package argument with no browser, using --json option', t => { + _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) + _flatOptions.json = true + + fund(['.'], (err) => { + t.ifError(err, 'should not error out') + t.deepEqual( + JSON.parse(printUrl), + { + title: 'individual funding available at the following URL', + url: 'http://example.com/donate' + }, + 'should open funding url using json output' + ) + + _flatOptions.json = false + printUrl = '' + t.end() + }) +}) + +test('fund using package info fetch from registry', t => { + _flatOptions.prefix = t.testdir({}) + + fund(['ntl'], (err) => { + t.ifError(err, 'should not error out') + t.match( + printUrl, + /http:\/\/example.com\/pacote/, + 'should open funding url that was loaded from registry manifest' + ) + + printUrl = '' + t.end() + }) +}) + +test('fund tries to use package info fetch from registry but registry has nothing', t => { + _flatOptions.prefix = t.testdir({}) + + fund(['foo'], (err) => { + t.equal(err.code, 'ENOFUND', 'should have ENOFUND error code') + t.equal( + err.message, + 'No valid funding method available for: foo', + 'should have no valid funding message' + ) + + printUrl = '' + t.end() + }) +}) + +test('fund but target module has no funding info', t => { + _flatOptions.prefix = t.testdir(nestedNoFundingPackages) + + fund(['foo'], (err) => { + t.equal(err.code, 'ENOFUND', 'should have ENOFUND error code') + t.equal( + err.message, + 'No valid funding method available for: foo', + 'should have no valid funding message' + ) + + result = '' + t.end() + }) +}) + +test('fund using bad which value', t => { + _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) + _flatOptions.which = 3 + + fund(['bar'], (err) => { + t.equal(err.code, 'EFUNDNUMBER', 'should have EFUNDNUMBER error code') + t.equal( + err.message, + '`npm fund [<@scope>/] [--which=fundingSourceNumber]` must be given a positive integer', + 'should have bad which option error message' + ) + + _flatOptions.which = undefined + result = '' + t.end() + }) +}) + +test('fund pkg missing version number', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'foo', + funding: 'http://example.com/foo' + }) + }) + + fund([], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(result, 'should print name only') + result = '' + t.end() + }) +}) + +test('fund a package throws on openUrl', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'foo', + version: '1.0.0', + funding: 'http://npmjs.org' + }) + }) + + fund(['.'], (err) => { + t.equal(err.message, 'ERROR', 'should throw unknown error') + result = '' + t.end() + }) +}) + +test('fund a package with type and multiple sources', t => { + _flatOptions.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'foo', + funding: [ + { + type: 'Foo', + url: 'http://example.com/foo' + }, + { + type: 'Lorem', + url: 'http://example.com/foo-lorem' + } + ] + }) + }) + + fund(['.'], (err) => { + t.ifError(err, 'should not error out') + t.matchSnapshot(result, 'should print prompt select message') + + result = '' + t.end() + }) +}) diff --git a/test/tap/fund.js b/test/tap/fund.js deleted file mode 100644 index 5e7745dc52228..0000000000000 --- a/test/tap/fund.js +++ /dev/null @@ -1,553 +0,0 @@ -'use strict' - -const fs = require('fs') -const path = require('path') -const { exec, execSync } = require('child_process') - -const { test } = require('tap') -const bin = require.resolve('../../bin/npm-cli.js') -const isWindows = require('../../lib/utils/is-windows.js') - -const version = '1.0.0' -const funding = { - type: 'individual', - url: 'http://example.com/donate' -} - -const maintainerOwnsAllDeps = { - 'package.json': JSON.stringify({ - name: 'maintainer-owns-all-deps', - version, - funding, - dependencies: { - 'dep-foo': '*', - 'dep-bar': '*' - } - }), - node_modules: { - 'dep-foo': { - 'package.json': JSON.stringify({ - name: 'dep-foo', - version, - funding, - dependencies: { - 'dep-sub-foo': '*' - } - }), - node_modules: { - 'dep-sub-foo': { - 'package.json': JSON.stringify({ - name: 'dep-sub-foo', - version, - funding - }) - } - } - }, - 'dep-bar': { - 'package.json': JSON.stringify({ - name: 'dep-bar', - version, - funding - }) - } - } -} - -const nestedNoFundingPackages = { - 'package.json': JSON.stringify({ - name: 'nested-no-funding-packages', - version, - dependencies: { - foo: '*' - }, - devDependencies: { - lorem: '*' - } - }), - node_modules: { - foo: { - 'package.json': JSON.stringify({ - name: 'foo', - version, - dependencies: { - bar: '*' - } - }), - node_modules: { - bar: { - 'package.json': JSON.stringify({ - name: 'bar', - version, - funding - }), - node_modules: { - 'sub-bar': { - 'package.json': JSON.stringify({ - name: 'sub-bar', - version, - funding: 'https://example.com/sponsor' - }) - } - } - } - } - }, - lorem: { - 'package.json': JSON.stringify({ - name: 'lorem', - version, - funding: { - url: 'https://example.com/lorem' - } - }) - } - } -} - -const nestedMultipleFundingPackages = { - 'package.json': JSON.stringify({ - name: 'nested-multiple-funding-packages', - version, - funding: [ - 'https://one.example.com', - 'https://two.example.com' - ], - dependencies: { - foo: '*' - }, - devDependencies: { - bar: '*' - } - }), - node_modules: { - foo: { - 'package.json': JSON.stringify({ - name: 'foo', - version, - funding: [ - 'http://example.com', - { url: 'http://sponsors.example.com/me' }, - 'http://collective.example.com' - ] - }) - }, - bar: { - 'package.json': JSON.stringify({ - name: 'bar', - version, - funding: [ - 'http://collective.example.com', - { url: 'http://sponsors.example.com/you' } - ] - }) - } - } -} - -const conflictingFundingPackages = { - 'package.json': JSON.stringify({ - name: 'conflicting-funding-packages', - version, - dependencies: { - foo: '1.0.0' - }, - devDependencies: { - bar: '1.0.0' - } - }), - node_modules: { - foo: { - 'package.json': JSON.stringify({ - name: 'foo', - version: '1.0.0', - funding: 'http://example.com/1' - }) - }, - bar: { - node_modules: { - foo: { - 'package.json': JSON.stringify({ - name: 'foo', - version: '2.0.0', - funding: 'http://example.com/2' - }) - } - }, - 'package.json': JSON.stringify({ - name: 'bar', - version: '1.0.0', - dependencies: { - foo: '2.0.0' - } - }) - } - } -} - -test('fund with no package containing funding', t => { - const cwd = t.testdir({ - 'package.json': JSON.stringify({ - name: 'no-funding-package', - version: '0.0.0' - }) - }) - const stdout = execSync( - `${process.execPath} ${bin} fund --unicode=false`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should print empty funding info') - t.end() -}) - -test('fund in which same maintainer owns all its deps', t => { - const cwd = t.testdir(maintainerOwnsAllDeps) - const stdout = execSync( - `${process.execPath} ${bin} fund --unicode=false`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should print stack packages together') - t.end() -}) - -test('fund in which same maintainer owns all its deps, using --json option', t => { - const cwd = t.testdir(maintainerOwnsAllDeps) - const stdout = execSync( - `${process.execPath} ${bin} fund --json`, - { cwd } - ) - t.deepEqual( - JSON.parse(stdout.toString()), - { - length: 3, - name: 'maintainer-owns-all-deps', - version: '1.0.0', - funding: { type: 'individual', url: 'http://example.com/donate' }, - dependencies: { - 'dep-bar': { - version: '1.0.0', - funding: { type: 'individual', url: 'http://example.com/donate' } - }, - 'dep-foo': { - version: '1.0.0', - funding: { type: 'individual', url: 'http://example.com/donate' }, - dependencies: { - 'dep-sub-foo': { - version: '1.0.0', - funding: { type: 'individual', url: 'http://example.com/donate' } - } - } - } - } - }, - 'should print stack packages together' - ) - t.end() -}) - -test('fund containing multi-level nested deps with no funding', t => { - const cwd = t.testdir(nestedNoFundingPackages) - const stdout = execSync( - `${process.execPath} ${bin} fund --unicode=false`, - { cwd } - ) - t.matchSnapshot( - stdout.toString(), - 'should omit dependencies with no funding declared' - ) - t.end() -}) - -test('fund containing multi-level nested deps with no funding, using --json option', t => { - const cwd = t.testdir(nestedNoFundingPackages) - const stdout = execSync( - `${process.execPath} ${bin} fund --json`, - { cwd } - ) - t.deepEqual( - JSON.parse(stdout.toString()), - { - length: 2, - name: 'nested-no-funding-packages', - version: '1.0.0', - dependencies: { - lorem: { - version: '1.0.0', - funding: { url: 'https://example.com/lorem' } - }, - bar: { - version: '1.0.0', - funding: { type: 'individual', url: 'http://example.com/donate' } - } - } - }, - 'should omit dependencies with no funding declared in json output' - ) - t.end() -}) - -test('fund containing multi-level nested deps with no funding, using --json option', t => { - const cwd = t.testdir(nestedMultipleFundingPackages) - const stdout = execSync( - `${process.execPath} ${bin} fund --json`, - { cwd } - ) - t.deepEqual( - JSON.parse(stdout.toString()), - { - length: 2, - name: 'nested-multiple-funding-packages', - version: '1.0.0', - funding: [ - { - url: 'https://one.example.com' - }, - { - url: 'https://two.example.com' - } - ], - dependencies: { - bar: { - version: '1.0.0', - funding: [ - { - url: 'http://collective.example.com' - }, - { - url: 'http://sponsors.example.com/you' - } - ] - }, - foo: { - version: '1.0.0', - funding: [ - { - url: 'http://example.com' - }, - { - url: 'http://sponsors.example.com/me' - }, - { - url: 'http://collective.example.com' - } - ] - } - } - }, - 'should list multiple funding entries in json output' - ) - t.end() -}) - -test('fund does not support global', t => { - t.plan(1) - try { - execSync(`${process.execPath} ${bin} fund --global`, { stdio: 'pipe' }) - } catch (err) { - t.match( - err.toString(), - 'code EFUNDGLOBAL', - 'should throw EFUNDGLOBAL error' - ) - } -}) - -test('fund using package argument with no browser', t => { - const cwd = t.testdir(maintainerOwnsAllDeps) - const stdout = execSync( - `${process.execPath} ${bin} fund . --no-browser`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should open funding url') - t.end() -}) - -test('fund does not support global, using --json option', t => { - exec( - `${process.execPath} ${bin} fund --global --json`, - (_, stdout) => { - t.deepEqual( - JSON.parse(stdout.toString()), - { - error: { - code: 'EFUNDGLOBAL', - summary: '`npm fund` does not support global packages', - detail: '' - } - }, - 'should throw EFUNDGLOBAL error using json output' - ) - t.end() - } - ) -}) - -test('fund using string shorthand', t => { - const cwd = t.testdir({ - 'package.json': JSON.stringify({ - name: 'funding-string-shorthand', - version: '0.0.0', - funding: 'https://example.com/sponsor' - }) - }) - const stdout = execSync( - `${process.execPath} ${bin} fund . --no-browser`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should open string-only url') - t.end() -}) - -test('fund using nested packages with multiple sources', t => { - const cwd = t.testdir(nestedMultipleFundingPackages) - const stdout = execSync( - `${process.execPath} ${bin} fund .`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should prompt with all available URLs') - t.end() -}) - -test('fund using symlink ref', t => { - const cwd = t.testdir({ - 'package.json': JSON.stringify({ - name: 'using-symlink-ref', - version: '1.0.0' - }), - 'a': { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - funding: 'http://example.com/a' - }) - }, - node_modules: { - a: t.fixture('symlink', '../a') - } - }) - - // using symlinked ref - t.match( - execSync( - `${process.execPath} ${bin} fund ./node_modules/a --no-browser`, - { cwd } - ).toString(), - 'http://example.com/a', - 'should retrieve funding url from symlink' - ) - - // using target ref - t.match( - execSync( - `${process.execPath} ${bin} fund ./a --no-browser`, - { cwd } - ).toString(), - 'http://example.com/a', - 'should retrieve funding url from symlink target' - ) - - t.end() -}) - -test('fund using data from actual tree', t => { - const cwd = t.testdir({ - 'package.json': JSON.stringify({ - name: 'using-actual-tree', - version: '1.0.0' - }), - node_modules: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - funding: 'http://example.com/a' - }) - }, - b: { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - funding: 'http://example.com/b' - }), - node_modules: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.1', - funding: 'http://example.com/_AAA' - }) - } - } - } - } - }) - - // using symlinked ref - t.match( - execSync( - `${process.execPath} ${bin} fund a --no-browser`, - { cwd } - ).toString(), - 'http://example.com/_AAA', - 'should retrieve fund info from actual tree, using greatest version found' - ) - - t.end() -}) - -test('fund using nested packages with multiple sources, with a source number', t => { - const cwd = t.testdir(nestedMultipleFundingPackages) - const stdout = execSync( - `${process.execPath} ${bin} fund . --which=1 --no-browser`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should open the numbered URL') - t.end() -}) - -test('fund using pkg name while having conflicting versions', t => { - const cwd = t.testdir(conflictingFundingPackages) - const stdout = execSync( - `${process.execPath} ${bin} fund foo --which=1 --no-browser`, - { cwd } - ) - t.matchSnapshot(stdout.toString(), 'should open greatest version') - t.end() -}) - -test('fund using package argument with no browser, using --json option', t => { - const cwd = t.testdir(maintainerOwnsAllDeps) - const stdout = execSync( - `${process.execPath} ${bin} fund . --json --no-browser`, - { cwd } - ) - t.deepEqual( - JSON.parse(stdout.toString()), - { - title: 'individual funding available at the following URL', - url: 'http://example.com/donate' - }, - 'should open funding url using json output' - ) - t.end() -}) - -if (!isWindows) { - test('mock browser using package name argument', function (t) { - const cwd = t.testdir(maintainerOwnsAllDeps) - const fakeBrowser = path.join(cwd, '_script.sh') - const outFile = path.join(cwd, '_output') - - const s = `#!/usr/bin/env bash\necho "$@" > ${outFile}\n` - fs.writeFileSync(fakeBrowser, s) - fs.chmodSync(fakeBrowser, '0755') - - execSync( - `${process.execPath} ${bin} fund . --loglevel=silent --browser=${fakeBrowser}`, - { cwd } - ) - var res = fs.readFileSync(outFile, 'utf8') - t.equal(res, 'http://example.com/donate\n') - t.end() - }) -}