// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/publish_check_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; void main() { group('PublishCheckCommand tests', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late RecordingProcessRunner processRunner; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final PublishCheckCommand publishCheckCommand = PublishCheckCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, ); runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); runner.addCommand(publishCheckCommand); }); test('publish check all packages', () async { final RepositoryPackage plugin1 = createFakePlugin( 'plugin_tools_test_package_a', packagesDir, examples: [], ); final RepositoryPackage plugin2 = createFakePlugin( 'plugin_tools_test_package_b', packagesDir, examples: [], ); await runCapturingPrint(runner, ['publish-check']); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], plugin1.path), ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], plugin2.path), ])); }); test('publish prepares dependencies of examples (when present)', () async { final RepositoryPackage plugin1 = createFakePlugin( 'plugin_tools_test_package_a', packagesDir, examples: ['example1', 'example2'], ); final RepositoryPackage plugin2 = createFakePlugin( 'plugin_tools_test_package_b', packagesDir, examples: [], ); await runCapturingPrint(runner, ['publish-check']); // For plugin1, these are the expected pub get calls that will happen final Iterable pubGetCalls = plugin1.getExamples().map((RepositoryPackage example) { return ProcessCall( getFlutterCommand(mockPlatform), const ['pub', 'get'], example.path, ); }); expect(pubGetCalls, hasLength(2)); expect( processRunner.recordedCalls, orderedEquals([ // plugin1 has 2 examples, so there's some 'dart pub get' calls. ...pubGetCalls, ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], plugin1.path), // plugin2 has no examples, so there's no extra 'dart pub get' calls. ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], plugin2.path), ]), ); }); test('fail on negative test', () async { createFakePlugin('plugin_tools_test_package_a', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(), ['pub', 'get']), FakeProcessInfo(MockProcess(exitCode: 1, stdout: 'Some error from pub'), ['pub', 'publish']) ]; Error? commandError; final List output = await runCapturingPrint( runner, ['publish-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Some error from pub'), contains('Unable to publish plugin_tools_test_package_a'), ]), ); }); test('fail on bad pubspec', () async { final RepositoryPackage package = createFakePlugin('c', packagesDir); await package.pubspecFile.writeAsString('bad-yaml'); Error? commandError; final List output = await runCapturingPrint( runner, ['publish-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('No valid pubspec found.'), ]), ); }); test('fails if AUTHORS is missing', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir); package.authorsFile.deleteSync(); Error? commandError; final List output = await runCapturingPrint( runner, ['publish-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains( 'No AUTHORS file found. Packages must include an AUTHORS file.'), ]), ); }); test('does not require AUTHORS for third-party', () async { final RepositoryPackage package = createFakePackage( 'a_package', packagesDir.parent .childDirectory('third_party') .childDirectory('packages')); package.authorsFile.deleteSync(); final List output = await runCapturingPrint(runner, ['publish-check']); expect( output, containsAllInOrder([ contains('Running for a_package'), ]), ); }); test('pass on prerelease if --allow-pre-release flag is on', () async { createFakePlugin('d', packagesDir); final MockProcess process = MockProcess( exitCode: 1, stdout: 'Package has 1 warning.\n' 'Packages with an SDK constraint on a pre-release of the Dart ' 'SDK should themselves be published as a pre-release version.'); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(), ['pub', 'get']), FakeProcessInfo(process, ['pub', 'publish']), ]; expect( runCapturingPrint( runner, ['publish-check', '--allow-pre-release']), completes); }); test('fail on prerelease if --allow-pre-release flag is off', () async { createFakePlugin('d', packagesDir); final MockProcess process = MockProcess( exitCode: 1, stdout: 'Package has 1 warning.\n' 'Packages with an SDK constraint on a pre-release of the Dart ' 'SDK should themselves be published as a pre-release version.'); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(), ['pub', 'get']), FakeProcessInfo(process, ['pub', 'publish']), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['publish-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains( 'Packages with an SDK constraint on a pre-release of the Dart SDK'), contains('Unable to publish d'), ]), ); }); test('Success message on stderr is not printed as an error', () async { createFakePlugin('d', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(), ['pub', 'get']), FakeProcessInfo(MockProcess(stdout: 'Package has 0 warnings.'), ['pub', 'publish']), ]; final List output = await runCapturingPrint(runner, ['publish-check']); expect(output, isNot(contains(contains('ERROR:')))); }); test( 'runs validation even for packages that are already published and reports failure', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, version: '0.1.0'); final MockClient mockClient = MockClient((http.Request request) async { if (request.url.pathSegments.last == 'a_package.json') { return http.Response( json.encode({ 'name': 'a_package', 'versions': [ '0.0.1', '0.1.0', ], }), 200); } return http.Response('', 500); }); runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); runner.addCommand(PublishCheckCommand(packagesDir, platform: mockPlatform, processRunner: processRunner, httpClient: mockClient)); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(exitCode: 1, stdout: 'Some error from pub'), ['pub', 'publish']) ]; Error? commandError; final List output = await runCapturingPrint( runner, ['publish-check'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Unable to publish a_package'), ]), ); expect( processRunner.recordedCalls, contains( ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], package.path), )); }); test('skips packages that are marked as not for publishing', () async { createFakePackage('a_package', packagesDir, version: '0.1.0', publishTo: 'none'); final List output = await runCapturingPrint(runner, ['publish-check']); expect( output, containsAllInOrder([ contains('SKIPPING: Package is marked as unpublishable.'), ]), ); expect(processRunner.recordedCalls, isEmpty); }); test( 'runs validation even for packages that are already published and reports success', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, version: '0.1.0'); final MockClient mockClient = MockClient((http.Request request) async { if (request.url.pathSegments.last == 'a_package.json') { return http.Response( json.encode({ 'name': 'a_package', 'versions': [ '0.0.1', '0.1.0', ], }), 200); } return http.Response('', 500); }); runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); runner.addCommand(PublishCheckCommand(packagesDir, platform: mockPlatform, processRunner: processRunner, httpClient: mockClient)); final List output = await runCapturingPrint(runner, ['publish-check']); expect( output, containsAllInOrder([ contains( 'Package a_package version: 0.1.0 has already been published on pub.'), ]), ); expect( processRunner.recordedCalls, contains( ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], package.path), )); }); group('pre-publish script', () { test('runs if present', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, examples: []); package.prePublishScript.createSync(recursive: true); final List output = await runCapturingPrint(runner, [ 'publish-check', ]); expect( output, containsAllInOrder([ contains('Running pre-publish hook tool/pre_publish.dart...'), ]), ); expect( processRunner.recordedCalls, containsAllInOrder([ ProcessCall( 'dart', const [ 'pub', 'get', ], package.directory.path), ProcessCall( 'dart', const [ 'run', 'tool/pre_publish.dart', ], package.directory.path), ])); }); test('runs before publish --dry-run', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, examples: []); package.prePublishScript.createSync(recursive: true); final List output = await runCapturingPrint(runner, [ 'publish-check', ]); expect( output, containsAllInOrder([ contains('Running pre-publish hook tool/pre_publish.dart...'), ]), ); expect( processRunner.recordedCalls, containsAllInOrder([ ProcessCall( 'dart', const [ 'run', 'tool/pre_publish.dart', ], package.directory.path), ProcessCall( 'flutter', const [ 'pub', 'publish', '--', '--dry-run', ], package.directory.path), ])); }); test('causes command failure if it fails', () async { final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, isFlutter: true, examples: []); package.prePublishScript.createSync(recursive: true); processRunner.mockProcessesForExecutable['dart'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['run']), // run tool/pre_publish.dart ]; Error? commandError; final List output = await runCapturingPrint(runner, [ 'publish-check', ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Pre-publish script failed.'), ]), ); expect( processRunner.recordedCalls, containsAllInOrder([ ProcessCall( getFlutterCommand(mockPlatform), const [ 'pub', 'get', ], package.directory.path), ProcessCall( 'dart', const [ 'run', 'tool/pre_publish.dart', ], package.directory.path), ])); }); }); test( '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', () async { const Map httpResponseA = { 'name': 'a', 'versions': [ '0.0.1', '0.1.0', ], }; const Map httpResponseB = { 'name': 'b', 'versions': [ '0.0.1', '0.1.0', '0.2.0', ], }; final MockClient mockClient = MockClient((http.Request request) async { if (request.url.pathSegments.last == 'no_publish_a.json') { return http.Response(json.encode(httpResponseA), 200); } else if (request.url.pathSegments.last == 'no_publish_b.json') { return http.Response(json.encode(httpResponseB), 200); } return http.Response('', 500); }); final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); runner.addCommand(command); createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); expect(output.first, r''' { "status": "no-publish", "humanMessage": [ "\n============================================================\n|| Running for no_publish_a\n============================================================\n", "Running pub publish --dry-run:", "Package no_publish_a version: 0.1.0 has already been published on pub.", "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "Running pub publish --dry-run:", "Package no_publish_b version: 0.2.0 has already been published on pub.", "\n", "------------------------------------------------------------", "Run overview:", " no_publish_a - ran", " no_publish_b - ran", "", "Ran for 2 package(s)", "\n", "No issues found!" ] }'''); }); test( '--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.', () async { const Map httpResponseA = { 'name': 'a', 'versions': [ '0.0.1', '0.1.0', ], }; const Map httpResponseB = { 'name': 'b', 'versions': [ '0.0.1', '0.1.0', ], }; final MockClient mockClient = MockClient((http.Request request) async { if (request.url.pathSegments.last == 'no_publish_a.json') { return http.Response(json.encode(httpResponseA), 200); } else if (request.url.pathSegments.last == 'no_publish_b.json') { return http.Response(json.encode(httpResponseB), 200); } return http.Response('', 500); }); final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); runner.addCommand(command); createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); expect(output.first, r''' { "status": "needs-publish", "humanMessage": [ "\n============================================================\n|| Running for no_publish_a\n============================================================\n", "Running pub publish --dry-run:", "Package no_publish_a version: 0.1.0 has already been published on pub.", "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "Running pub publish --dry-run:", "Package no_publish_b is able to be published.", "\n", "------------------------------------------------------------", "Run overview:", " no_publish_a - ran", " no_publish_b - ran", "", "Ran for 2 package(s)", "\n", "No issues found!" ] }'''); }); test( '--machine: Log correct JSON, if there is at least 1 plugin contains error.', () async { const Map httpResponseA = { 'name': 'a', 'versions': [ '0.0.1', '0.1.0', ], }; const Map httpResponseB = { 'name': 'b', 'versions': [ '0.0.1', '0.1.0', ], }; final MockClient mockClient = MockClient((http.Request request) async { print('url ${request.url}'); print(request.url.pathSegments.last); if (request.url.pathSegments.last == 'no_publish_a.json') { return http.Response(json.encode(httpResponseA), 200); } else if (request.url.pathSegments.last == 'no_publish_b.json') { return http.Response(json.encode(httpResponseB), 200); } return http.Response('', 500); }); final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); runner.addCommand(command); final RepositoryPackage plugin = createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); await plugin.pubspecFile.writeAsString('bad-yaml'); bool hasError = false; final List output = await runCapturingPrint( runner, ['publish-check', '--machine'], errorHandler: (Error error) { expect(error, isA()); hasError = true; }); expect(hasError, isTrue); expect(output.first, contains(r''' { "status": "error", "humanMessage": [ "\n============================================================\n|| Running for no_publish_a\n============================================================\n", "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException:''')); // This is split into two checks since the details of the YamlException // aren't controlled by this package, so asserting its exact format would // make the test fragile to irrelevant changes in those details. expect(output.first, contains(r''' "No valid pubspec found.", "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "url https://pub.dev/packages/no_publish_b.json", "no_publish_b.json", "Running pub publish --dry-run:", "Package no_publish_b is able to be published.", "\n", "The following packages had errors:", " no_publish_a", "See above for full details." ] }''')); }); }); }