Repository: VeryGoodOpenSource/very_good_cli
Pull request #883 (2023-11-10 13:31:32)

feat!: detect licenses locally
<!--
  Thanks for contributing!
  Provide a description of your changes below and a general summary in the title
  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Status

**READY**

## Description

Instead of fetching, detect license locally. This change uses pana's non-public API intentionally, refer to https://github.com/dart-lang/pana/issues/1277#

Now there is no need of a network connection, instead there is a need for a `package_config.json` (generated on `pub get`) to know the paths of the cached packages.

There is no public facing API change, instead is an internal implementation change. Hence, there is a small chance that previous detections might differ from current detections.

## Demonstration

**After the change**:

https://github.com/VeryGoodOpenSource/very_good_cli/assets/44524995/17042f84-b3de-4f1a-9dd9-a912c8a02ee2

**Before the change**:

https://github.com/VeryGoodOpenSource/very_good_cli/assets/44524995/c82bc8f2-0534-4a4e-9006-7ea0689a8afb


## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [X] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore



Patch:
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 53418ae..b3f013e 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -8,7 +8,6 @@ import 'package:pub_updater/pub_updater.dart';
 import 'package:universal_io/io.dart';
 import 'package:very_good_cli/src/commands/commands.dart';
 import 'package:very_good_cli/src/logger_extension.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
 import 'package:very_good_cli/src/version.dart';
 
 /// The package name.
@@ -23,7 +22,6 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
     Logger? logger,
     PubUpdater? pubUpdater,
     Map<String, String>? environment,
-    @visibleForTesting PubLicense? pubLicense,
   })  : _logger = logger ?? Logger(),
         _pubUpdater = pubUpdater ?? PubUpdater(),
         _environment = environment ?? Platform.environment,
@@ -39,7 +37,7 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
         help: 'Noisy logging, including all shell commands executed.',
       );
     addCommand(CreateCommand(logger: _logger));
-    addCommand(PackagesCommand(logger: _logger, pubLicense: pubLicense));
+    addCommand(PackagesCommand(logger: _logger));
     addCommand(TestCommand(logger: _logger));
     addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater));
   }
diff --git a/lib/src/commands/packages/commands/check/check.dart b/lib/src/commands/packages/commands/check/check.dart
index 9f9b1f8..07ea1c5 100644
--- a/lib/src/commands/packages/commands/check/check.dart
+++ b/lib/src/commands/packages/commands/check/check.dart
@@ -1,7 +1,6 @@
 import 'package:args/command_runner.dart';
 import 'package:mason/mason.dart';
 import 'package:very_good_cli/src/commands/packages/commands/check/commands/commands.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
 
 /// {@template packages_check_command}
 /// `very_good packages check` command for performing checks in a Dart or
@@ -11,10 +10,9 @@ class PackagesCheckCommand extends Command<int> {
   /// {@macro packages_check_command}
   PackagesCheckCommand({
     Logger? logger,
-    PubLicense? pubLicense,
   }) {
     addSubcommand(
-      PackagesCheckLicensesCommand(logger: logger, pubLicense: pubLicense),
+      PackagesCheckLicensesCommand(logger: logger),
     );
   }
 
diff --git a/lib/src/commands/packages/commands/check/commands/licenses.dart b/lib/src/commands/packages/commands/check/commands/licenses.dart
index 7a902ac..ba2ca85 100644
--- a/lib/src/commands/packages/commands/check/commands/licenses.dart
+++ b/lib/src/commands/packages/commands/check/commands/licenses.dart
@@ -2,13 +2,36 @@ import 'dart:io';
 
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
+import 'package:collection/collection.dart';
 import 'package:mason/mason.dart';
 import 'package:meta/meta.dart';
+import 'package:package_config/package_config.dart' as package_config;
+
+// We rely on PANA's license detection algorithm to retrieve licenses from
+// packages.
+//
+// This license detection algorithm is not exposed as a public API, so we have
+// to import it directly.
+//
+// See also:
+//
+// * [PANA's faster license detection GitHub issue](https://github.com/dart-lang/pana/issues/1277)
+// ignore: implementation_imports
+import 'package:pana/src/license_detection/license_detector.dart' as detector;
 import 'package:path/path.dart' as path;
 import 'package:pubspec_lock/pubspec_lock.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
 import 'package:very_good_cli/src/pub_license/spdx_license.gen.dart';
 
+/// Overrides the [package_config.findPackageConfig] function for testing.
+@visibleForTesting
+Future<package_config.PackageConfig?> Function(
+  Directory directory,
+)? findPackageConfigOverride;
+
+/// Overrides the [detector.detectLicense] function for testing.
+@visibleForTesting
+Future<detector.Result> Function(String, double)? detectLicenseOverride;
+
 /// The basename of the pubspec lock file.
 @visibleForTesting
 const pubspecLockBasename = 'pubspec.lock';
@@ -24,6 +47,16 @@ final licenseDocumentationUri = Uri.parse(
   'https://cli.vgv.dev/docs/commands/check_licenses',
 );
 
+/// The detection threshold used by [detector.detectLicense].
+///
+/// This value is used to determine the confidence threshold for detecting
+/// licenses. The value should match the default value used by PANA.
+///
+/// See also:
+///
+/// * [PANA's default threshold value](https://github.com/dart-lang/pana/blob/b598d45051ba4e028e9021c2aeb9c04e4335de76/lib/src/license.dart#L48)
+const _defaultDetectionThreshold = 0.95;
+
 /// Defines a [Map] with dependencies as keys and their licenses as values.
 ///
 /// If a dependency's license failed to be retrieved its license will be `null`.
@@ -40,9 +73,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
   /// {@macro packages_check_licenses_command}
   PackagesCheckLicensesCommand({
     Logger? logger,
-    PubLicense? pubLicense,
-  })  : _logger = logger ?? Logger(),
-        _pubLicense = pubLicense ?? PubLicense() {
+  }) : _logger = logger ?? Logger() {
     argParser
       ..addFlag(
         'ignore-retrieval-failures',
@@ -80,8 +111,6 @@ class PackagesCheckLicensesCommand extends Command<int> {
 
   final Logger _logger;
 
-  final PubLicense _pubLicense;
-
   @override
   String get description =>
       "Check packages' licenses in a Dart or Flutter project.";
@@ -128,6 +157,13 @@ class PackagesCheckLicensesCommand extends Command<int> {
 
     final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
     final targetPath = path.normalize(Directory(target).absolute.path);
+    final targetDirectory = Directory(targetPath);
+    if (!targetDirectory.existsSync()) {
+      _logger.err(
+        '''Could not find directory at $targetPath. Specify a valid path to a Dart or Flutter project.''',
+      );
+      return ExitCode.noInput.code;
+    }
 
     final progress = _logger.progress('Checking licenses on $targetPath');
 
@@ -169,28 +205,70 @@ class PackagesCheckLicensesCommand extends Command<int> {
       return ExitCode.usage.code;
     }
 
+    final packageConfig = await _tryFindPackageConfig(targetDirectory);
+    if (packageConfig == null) {
+      progress.cancel();
+      _logger.err(
+        '''Could not find a valid package config in $targetPath. Run `dart pub get` or `flutter pub get` to generate one.''',
+      );
+      return ExitCode.noInput.code;
+    }
+
     final licenses = <String, Set<String>?>{};
+    final detectLicense = detectLicenseOverride ?? detector.detectLicense;
     for (final dependency in filteredDependencies) {
       progress.update(
         '''Collecting licenses from ${licenses.length + 1} out of ${filteredDependencies.length} ${filteredDependencies.length == 1 ? 'package' : 'packages'}''',
       );
 
       final dependencyName = dependency.package();
-      Set<String>? rawLicense;
-      try {
-        rawLicense = await _pubLicense.getLicense(dependencyName);
-      } on PubLicenseException catch (e) {
-        final errorMessage = '[$dependencyName] ${e.message}';
+      final cachePackageEntry = packageConfig.packages
+          .firstWhereOrNull((package) => package.name == dependencyName);
+      if (cachePackageEntry == null) {
+        final errorMessage =
+            '''[$dependencyName] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.''';
         if (!ignoreFailures) {
           progress.cancel();
           _logger.err(errorMessage);
-          return ExitCode.unavailable.code;
+          return ExitCode.noInput.code;
         }
 
         _logger.err('\n$errorMessage');
+        licenses[dependencyName] = {SpdxLicense.$unknown.value};
+        continue;
+      }
+
+      final packagePath = path.normalize(cachePackageEntry.root.path);
+      final packageDirectory = Directory(packagePath);
+      if (!packageDirectory.existsSync()) {
+        final errorMessage =
+            '''[$dependencyName] Could not find package directory at $packagePath.''';
+        if (!ignoreFailures) {
+          progress.cancel();
+          _logger.err(errorMessage);
+          return ExitCode.noInput.code;
+        }
+
+        _logger.err('\n$errorMessage');
+        licenses[dependencyName] = {SpdxLicense.$unknown.value};
+        continue;
+      }
+
+      final licenseFile = File(path.join(packagePath, 'LICENSE'));
+      if (!licenseFile.existsSync()) {
+        licenses[dependencyName] = {SpdxLicense.$unknown.value};
+        continue;
+      }
+
+      final licenseFileContent = licenseFile.readAsStringSync();
+
+      late final detector.Result detectorResult;
+      try {
+        detectorResult =
+            await detectLicense(licenseFileContent, _defaultDetectionThreshold);
       } catch (e) {
         final errorMessage =
-            '[$dependencyName] Unexpected failure with error: $e';
+            '''[$dependencyName] Failed to detect license from $packagePath: $e''';
         if (!ignoreFailures) {
           progress.cancel();
           _logger.err(errorMessage);
@@ -198,9 +276,15 @@ class PackagesCheckLicensesCommand extends Command<int> {
         }
 
         _logger.err('\n$errorMessage');
-      } finally {
-        licenses[dependencyName] = rawLicense;
+        licenses[dependencyName] = {SpdxLicense.$unknown.value};
+        continue;
       }
+
+      final rawLicense = detectorResult.matches
+          // ignore: invalid_use_of_visible_for_testing_member
+          .map((match) => match.license.identifier)
+          .toSet();
+      licenses[dependencyName] = rawLicense;
     }
 
     late final _BannedDependencyLicenseMap? bannedDependencies;
@@ -246,6 +330,23 @@ PubspecLock? _tryParsePubspecLock(File pubspecLockFile) {
   }
 }
 
+/// Attempts to find a [package_config.PackageConfig] using
+/// [package_config.findPackageConfig].
+///
+/// If [package_config.findPackageConfig] fails to find a package config `null`
+/// is returned.
+Future<package_config.PackageConfig?> _tryFindPackageConfig(
+  Directory directory,
+) async {
+  try {
+    final findPackageConfig =
+        findPackageConfigOverride ?? package_config.findPackageConfig;
+    return await findPackageConfig(directory);
+  } catch (error) {
+    return null;
+  }
+}
+
 /// Verifies that all [licenses] are valid license inputs.
 ///
 /// Valid license inputs are:
diff --git a/lib/src/commands/packages/packages.dart b/lib/src/commands/packages/packages.dart
index 6d48319..7afeec6 100644
--- a/lib/src/commands/packages/packages.dart
+++ b/lib/src/commands/packages/packages.dart
@@ -2,16 +2,15 @@ import 'package:args/command_runner.dart';
 import 'package:mason/mason.dart';
 import 'package:very_good_cli/src/commands/packages/commands/check/check.dart';
 import 'package:very_good_cli/src/commands/packages/commands/commands.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
 
 /// {@template packages_command}
 /// `very_good packages` command for managing packages.
 /// {@endtemplate}
 class PackagesCommand extends Command<int> {
   /// {@macro packages_command}
-  PackagesCommand({Logger? logger, PubLicense? pubLicense}) {
+  PackagesCommand({Logger? logger}) {
     addSubcommand(PackagesGetCommand(logger: logger));
-    addSubcommand(PackagesCheckCommand(logger: logger, pubLicense: pubLicense));
+    addSubcommand(PackagesCheckCommand(logger: logger));
   }
 
   @override
diff --git a/lib/src/pub_license/pub_license.dart b/lib/src/pub_license/pub_license.dart
deleted file mode 100644
index e31c2bb..0000000
--- a/lib/src/pub_license/pub_license.dart
+++ /dev/null
@@ -1,137 +0,0 @@
-/// Enables checking a package's license from pub.dev.
-///
-/// This library is intended to be used by Very Good CLI to help extracting
-/// license information. The existence of this library is likely to be
-/// ephemeral. It may be obsolete once [pub.dev](https://pub.dev/) exposes
-/// stable license information in their official API; you may track the
-/// progress [here](https://github.com/dart-lang/pub-dev/issues/4717).
-library pub_license;
-
-import 'package:html/dom.dart' as html_dom;
-import 'package:html/parser.dart' as html_parser;
-import 'package:http/http.dart' as http;
-import 'package:meta/meta.dart';
-
-/// The pub.dev [Uri] used to retrieve the license of a package.
-Uri _pubPackageLicenseUri(String packageName) =>
-    Uri.parse('https://pub.dev/packages/$packageName/license');
-
-/// {@template pub_license_exception}
-/// An exception thrown by [PubLicense].
-/// {@endtemplate}
-class PubLicenseException implements Exception {
-  /// {@macro pub_license_exception}
-  const PubLicenseException(String message)
-      : message = '[pub_license] $message';
-
-  /// The exception message.
-  final String message;
-}
-
-/// The function signature for parsing HTML documents.
-@visibleForTesting
-typedef HtmlDocumentParse = html_dom.Document Function(
-  dynamic input, {
-  String? encoding,
-  bool generateSpans,
-  String? sourceUrl,
-});
-
-/// {@template pub_license}
-/// Enables checking pub.dev's hosted packages license.
-/// {@endtemplate}
-class PubLicense {
-  /// {@macro pub_license}
-  PubLicense({
-    @visibleForTesting http.Client? client,
-    @visibleForTesting HtmlDocumentParse? parse,
-  })  : _client = client ?? http.Client(),
-        _parse = parse ?? html_parser.parse;
-
-  final http.Client _client;
-
-  final html_dom.Document Function(
-    dynamic input, {
-    String? encoding,
-    bool generateSpans,
-    String? sourceUrl,
-  }) _parse;
-
-  /// Retrieves the license of a package.
-  ///
-  /// Some packages may have multiple licenses, hence a [Set] is returned.
-  ///
-  /// It may throw a [PubLicenseException] if:
-  /// * The response from pub.dev is not successful.
-  /// * The response body cannot be parsed.
-  Future<Set<String>> getLicense(String packageName) async {
-    final response = await _client.get(_pubPackageLicenseUri(packageName));
-
-    if (response.statusCode != 200) {
-      throw PubLicenseException(
-        '''Failed to retrieve the license of the package, received status code: ${response.statusCode}''',
-      );
-    }
-
-    late final html_dom.Document document;
-    try {
-      document = _parse(response.body);
-    } on html_parser.ParseError catch (e) {
-      throw PubLicenseException(
-        'Failed to parse the response body, received error: $e',
-      );
-    } catch (e) {
-      throw PubLicenseException(
-        '''An unknown error occurred when trying to parse the response body, received error: $e''',
-      );
-    }
-
-    return _scrapeLicense(document);
-  }
-}
-
-/// Scrapes the license from the pub.dev's package license page.
-///
-/// The expected HTML structure is:
-/// ```html
-/// <aside class="detail-info-box">
-///   <h3> ... </h3>
-///   <p> ... </p>
-///   <h3 class="title">License</h3>
-///   <p>
-///   <img/>
-///   MIT (<a href="/packages/very_good_cli/license">LICENSE</a>)
-///   </p>
-/// </aside>
-/// ```
-///
-/// It may throw a [PubLicenseException] if:
-/// * The detail info box is not found.
-/// * The license header is not found.
-Set<String> _scrapeLicense(html_dom.Document document) {
-  final detailInfoBox = document.querySelector('.detail-info-box');
-  if (detailInfoBox == null) {
-    throw const PubLicenseException(
-      '''Failed to scrape license because `.detail-info-box` was not found.''',
-    );
-  }
-
-  String? rawLicenseText;
-  for (var i = 0; i < detailInfoBox.children.length; i++) {
-    final child = detailInfoBox.children[i];
-
-    final headerText = child.text.trim().toLowerCase();
-    if (headerText == 'license') {
-      rawLicenseText = detailInfoBox.children[i + 1].text.trim();
-      break;
-    }
-  }
-  if (rawLicenseText == null) {
-    throw const PubLicenseException(
-      '''Failed to scrape license because the license header was not found.''',
-    );
-  }
-
-  final licenseText = rawLicenseText.split('(').first.trim();
-  return licenseText.split(',').map((e) => e.trim()).toSet();
-}
diff --git a/pubspec.yaml b/pubspec.yaml
index 44f0359..699ece3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,12 +15,12 @@ dependencies:
   cli_completion: ^0.4.0
   collection: ^1.17.1
   glob: ^2.0.2
-  html: ^0.15.4 # This dependency is temporary and should be removed once pub_license is obsolete.
-  http: ^1.1.0 # This dependency is temporary and should be removed once pub_license is obsolete.
   lcov_parser: ^0.1.2
   mason: 0.1.0-dev.51
   mason_logger: ^0.2.2
   meta: ^1.3.0
+  package_config: ^2.1.0
+  pana: 0.21.42 # Very Good CLI is using private PANA's license detector that might break in a minor version update.
   path: ^1.8.0
   pub_updater: ">=0.3.1 <0.5.0"
   pubspec_lock: ^3.0.2


Test patch:
diff --git a/test/helpers/command_helper.dart b/test/helpers/command_helper.dart
index 3bd1428..3920f25 100644
--- a/test/helpers/command_helper.dart
+++ b/test/helpers/command_helper.dart
@@ -4,7 +4,6 @@ import 'package:mason/mason.dart';
 import 'package:mocktail/mocktail.dart';
 import 'package:pub_updater/pub_updater.dart';
 import 'package:very_good_cli/src/command_runner.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
 
 class _MockLogger extends Mock implements Logger {}
 
@@ -12,8 +11,6 @@ class _MockProgress extends Mock implements Progress {}
 
 class _MockPubUpdater extends Mock implements PubUpdater {}
 
-class _MockPubLicense extends Mock implements PubLicense {}
-
 void Function() _overridePrint(void Function(List<String>) fn) {
   return () {
     final printLogs = <String>[];
@@ -34,7 +31,6 @@ void Function() withRunner(
     VeryGoodCommandRunner commandRunner,
     Logger logger,
     PubUpdater pubUpdater,
-    PubLicense pubLicense,
     List<String> printLogs,
   ) runnerFn,
 ) {
@@ -42,13 +38,11 @@ void Function() withRunner(
     final logger = _MockLogger();
     final progress = _MockProgress();
     final pubUpdater = _MockPubUpdater();
-    final pubLicense = _MockPubLicense();
     final progressLogs = <String>[];
     final commandRunner = VeryGoodCommandRunner(
       logger: logger,
       pubUpdater: pubUpdater,
       environment: {'CI': 'true'},
-      pubLicense: pubLicense,
     );
 
     when(() => progress.complete(any())).thenAnswer((_) {
@@ -62,9 +56,7 @@ void Function() withRunner(
         currentVersion: any(named: 'currentVersion'),
       ),
     ).thenAnswer((_) => Future.value(true));
-    when(() => pubLicense.getLicense(any()))
-        .thenAnswer((_) => Future.value({'MIT'}));
 
-    await runnerFn(commandRunner, logger, pubUpdater, pubLicense, printLogs);
+    await runnerFn(commandRunner, logger, pubUpdater, printLogs);
   });
 }
diff --git a/test/src/commands/create/commands/dart_cli_test.dart b/test/src/commands/create/commands/dart_cli_test.dart
index 85ed36d..a19ed00 100644
--- a/test/src/commands/create/commands/dart_cli_test.dart
+++ b/test/src/commands/create/commands/dart_cli_test.dart
@@ -83,8 +83,7 @@ void main() {
   group('create dart_cli', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'dart_cli', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/commands/dart_package_test.dart b/test/src/commands/create/commands/dart_package_test.dart
index 7ca3267..7e69b9f 100644
--- a/test/src/commands/create/commands/dart_package_test.dart
+++ b/test/src/commands/create/commands/dart_package_test.dart
@@ -83,8 +83,7 @@ void main() {
   group('create dart_package', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'dart_package', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/commands/docs_site_test.dart b/test/src/commands/create/commands/docs_site_test.dart
index 20ae50a..9471a61 100644
--- a/test/src/commands/create/commands/docs_site_test.dart
+++ b/test/src/commands/create/commands/docs_site_test.dart
@@ -82,8 +82,7 @@ void main() {
   group('create docs_site', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'docs_site', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/commands/flame_game_test.dart b/test/src/commands/create/commands/flame_game_test.dart
index b47fbfa..5455663 100644
--- a/test/src/commands/create/commands/flame_game_test.dart
+++ b/test/src/commands/create/commands/flame_game_test.dart
@@ -88,8 +88,7 @@ void main() {
   group('create flame_game', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'flame_game', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/commands/flutter_app_test.dart b/test/src/commands/create/commands/flutter_app_test.dart
index 51510be..4bde545 100644
--- a/test/src/commands/create/commands/flutter_app_test.dart
+++ b/test/src/commands/create/commands/flutter_app_test.dart
@@ -94,8 +94,7 @@ void main() {
   group('create flutter_app', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'flutter_app', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/commands/flutter_package_test.dart b/test/src/commands/create/commands/flutter_package_test.dart
index 99ad665..85ee3b9 100644
--- a/test/src/commands/create/commands/flutter_package_test.dart
+++ b/test/src/commands/create/commands/flutter_package_test.dart
@@ -81,8 +81,7 @@ void main() {
   group('create flutter_package', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'flutter_package', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/commands/flutter_plugin_test.dart b/test/src/commands/create/commands/flutter_plugin_test.dart
index 96a2e43..cd41aa9 100644
--- a/test/src/commands/create/commands/flutter_plugin_test.dart
+++ b/test/src/commands/create/commands/flutter_plugin_test.dart
@@ -94,8 +94,7 @@ void main() {
   group('create flutter_plugin', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result =
             await commandRunner.run(['create', 'flutter_plugin', '--help']);
         expect(printLogs, equals(expectedUsage));
diff --git a/test/src/commands/create/create_test.dart b/test/src/commands/create/create_test.dart
index 655c86e..897d7fb 100644
--- a/test/src/commands/create/create_test.dart
+++ b/test/src/commands/create/create_test.dart
@@ -32,8 +32,7 @@ void main() {
   group('create', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(['create', '--help']);
         expect(printLogs, equals(expectedUsage));
         expect(result, equals(ExitCode.success.code));
diff --git a/test/src/commands/packages/commands/check/check_test.dart b/test/src/commands/packages/commands/check/check_test.dart
index 6a07b5a..90b8fa5 100644
--- a/test/src/commands/packages/commands/check/check_test.dart
+++ b/test/src/commands/packages/commands/check/check_test.dart
@@ -27,8 +27,7 @@ void main() {
 
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(
           [...commandArguments, '--help'],
         );
diff --git a/test/src/commands/packages/commands/check/commands/licenses_test.dart b/test/src/commands/packages/commands/check/commands/licenses_test.dart
index 6a71a31..08a69f5 100644
--- a/test/src/commands/packages/commands/check/commands/licenses_test.dart
+++ b/test/src/commands/packages/commands/check/commands/licenses_test.dart
@@ -3,15 +3,30 @@ import 'dart:io';
 import 'package:collection/collection.dart';
 import 'package:mason_logger/mason_logger.dart';
 import 'package:mocktail/mocktail.dart';
+import 'package:package_config/package_config.dart' as package_config;
+// ignore: implementation_imports
+import 'package:pana/src/license_detection/license_detector.dart' as detector;
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 import 'package:very_good_cli/src/commands/packages/commands/check/commands/commands.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
 
 import '../../../../../../helpers/helpers.dart';
 
 class _MockProgress extends Mock implements Progress {}
 
+class _MockResult extends Mock implements detector.Result {}
+
+// ignore: subtype_of_sealed_class
+class _MockLicenseMatch extends Mock implements detector.LicenseMatch {}
+
+// ignore: subtype_of_sealed_class
+class _MockLicenseWithNGrams extends Mock
+    implements detector.LicenseWithNGrams {}
+
+class _MockPackageConfig extends Mock implements package_config.PackageConfig {}
+
+class _MockPackage extends Mock implements package_config.Package {}
+
 const _expectedPackagesCheckLicensesUsage = [
   // ignore: no_adjacent_strings_in_list
   "Check packages' licenses in a Dart or Flutter project.\n"
@@ -40,8 +55,18 @@ void main() {
     const allowedArgument = '--allowed';
 
     late Progress progress;
+    late detector.Result detectorResult;
+    late package_config.PackageConfig packageConfig;
     late Directory tempDirectory;
 
+    late detector.LicenseMatch mitLicenseMatch;
+    late detector.LicenseMatch bsdLicenseMatch;
+
+    late package_config.Package veryGoodTestRunnerConfigPackage;
+    late package_config.Package cliCompletionConfigPackage;
+    late package_config.Package yamlConfigPackage;
+    late package_config.Package veryGoodAnalysisConfigPackage;
+
     setUpAll(() {
       registerFallbackValue('');
     });
@@ -49,14 +74,51 @@ void main() {
     setUp(() {
       progress = _MockProgress();
 
+      detectorResult = _MockResult();
+
+      detectLicenseOverride = (_, __) async => detectorResult;
+      addTearDown(() => detectLicenseOverride = null);
+
+      packageConfig = _MockPackageConfig();
+
+      findPackageConfigOverride = (_) async => packageConfig;
+      addTearDown(() => findPackageConfigOverride = null);
+
       tempDirectory = Directory.systemTemp.createTempSync();
       addTearDown(() => tempDirectory.deleteSync(recursive: true));
+
+      mitLicenseMatch = _MockLicenseMatch();
+      final mitLicenseWithNGrams = _MockLicenseWithNGrams();
+      when(() => mitLicenseMatch.license).thenReturn(mitLicenseWithNGrams);
+      when(() => mitLicenseWithNGrams.identifier).thenReturn('MIT');
+
+      bsdLicenseMatch = _MockLicenseMatch();
+      final bsdLicenseWithNGrams = _MockLicenseWithNGrams();
+      when(() => bsdLicenseMatch.license).thenReturn(bsdLicenseWithNGrams);
+      when(() => bsdLicenseWithNGrams.identifier).thenReturn('BSD');
+
+      final packages = {
+        'very_good_test_runner': veryGoodTestRunnerConfigPackage =
+            _MockPackage(),
+        'cli_completion': cliCompletionConfigPackage = _MockPackage(),
+        'yaml': yamlConfigPackage = _MockPackage(),
+        'very_good_analysis': veryGoodAnalysisConfigPackage = _MockPackage(),
+      };
+      for (final package in packages.entries) {
+        final name = package.key;
+        final packageConfig = package.value;
+
+        final licenseFile = File(path.join(tempDirectory.path, name, 'LICENSE'))
+          ..createSync(recursive: true)
+          ..writeAsStringSync(name);
+        when(() => packageConfig.name).thenReturn(name);
+        when(() => packageConfig.root).thenReturn(licenseFile.parent.uri);
+      }
     });
 
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(
           [...commandArguments, '--help'],
         );
@@ -79,8 +141,7 @@ void main() {
     group('throws usage exception', () {
       test(
         '''when too many rest arguments are provided''',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           final result = await commandRunner.run(
             [...commandArguments, 'arg1', 'arg2'],
           );
@@ -90,8 +151,7 @@ void main() {
 
       test(
         '''when allowed and forbidden are used simultaneously''',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           final result = await commandRunner.run(
             [...commandArguments, '--allowed', 'MIT', '--forbidden', 'BSD'],
           );
@@ -105,11 +165,14 @@ void main() {
       () {
         test(
           '''when there is a single hosted direct dependency and license''',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validPubspecLockContent);
 
+            when(() => packageConfig.packages)
+                .thenReturn([veryGoodTestRunnerConfigPackage]);
+            when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
             when(() => logger.progress(any())).thenReturn(progress);
 
             final result = await commandRunner.run(
@@ -133,15 +196,18 @@ void main() {
 
         test(
           '''when there are multiple hosted direct dependency and licenses''',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
-            when(() => pubLicense.getLicense(any()))
-                .thenAnswer((_) => Future.value({'MIT', 'BSD'}));
+            when(() => detectorResult.matches)
+                .thenReturn([mitLicenseMatch, bsdLicenseMatch]);
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
 
             final result = await commandRunner.run(
               [...commandArguments, tempDirectory.path],
@@ -169,11 +235,14 @@ void main() {
 
         test(
           '''when both allowed and forbidden are specified but left empty''',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validPubspecLockContent);
 
+            when(() => packageConfig.packages)
+                .thenReturn([veryGoodTestRunnerConfigPackage]);
+            when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
             when(() => logger.progress(any())).thenReturn(progress);
 
             final result = await commandRunner.run(
@@ -201,6 +270,42 @@ void main() {
             expect(result, equals(ExitCode.success.code));
           }),
         );
+
+        test(
+          'unknown when no license file is found',
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+            File(path.join(tempDirectory.path, pubspecLockBasename))
+                .writeAsStringSync(_validPubspecLockContent);
+
+            when(() => packageConfig.packages)
+                .thenReturn([veryGoodTestRunnerConfigPackage]);
+            final licenseFilePath = path.join(
+              tempDirectory.path,
+              veryGoodTestRunnerConfigPackage.name,
+              'LICENSE',
+            );
+            File(licenseFilePath).deleteSync();
+
+            when(() => logger.progress(any())).thenReturn(progress);
+
+            final result = await commandRunner.run(
+              [...commandArguments, tempDirectory.path],
+            );
+
+            verify(
+              () => progress.update(
+                'Collecting licenses from 1 out of 1 package',
+              ),
+            ).called(1);
+            verify(
+              () => progress.complete(
+                '''Retrieved 1 license from 1 package of type: unknown (1).''',
+              ),
+            ).called(1);
+
+            expect(result, equals(ExitCode.success.code));
+          }),
+        );
       },
     );
 
@@ -209,18 +314,31 @@ void main() {
 
       group('reports licenses', () {
         test(
-          'when a PubLicenseException is thrown',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          'when an unknown error is thrown',
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
             const failedDependencyName = 'very_good_test_runner';
-            const exception = PubLicenseException('message');
-            when(() => pubLicense.getLicense(failedDependencyName))
-                .thenThrow(exception);
+            const error = 'error';
+
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+
+            detectLicenseOverride = (name, __) async {
+              if (name == failedDependencyName) {
+                // ignore: only_throw_errors
+                throw error;
+              }
+
+              final detectorResult = _MockResult();
+              when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+              return detectorResult;
+            };
 
             final result = await commandRunner.run(
               [
@@ -230,8 +348,12 @@ void main() {
               ],
             );
 
+            final packagePath = path.join(
+              tempDirectory.path,
+              veryGoodTestRunnerConfigPackage.name,
+            );
             final errorMessage =
-                '''\n[$failedDependencyName] ${exception.message}''';
+                '''\n[$failedDependencyName] Failed to detect license from $packagePath: $error''';
             verify(() => logger.err(errorMessage)).called(1);
 
             verify(
@@ -246,7 +368,7 @@ void main() {
             ).called(1);
             verify(
               () => progress.complete(
-                'Retrieved 1 license from 2 packages of type: MIT (1).',
+                '''Retrieved 2 licenses from 2 packages of type: unknown (1) and MIT (1).''',
               ),
             ).called(1);
 
@@ -255,106 +377,156 @@ void main() {
         );
 
         test(
-          'when an unknown error is thrown',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          'when cached package path cannot be found',
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
-                .writeAsStringSync(_validMultiplePubspecLockContent);
+                .writeAsStringSync(_validPubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
-            const failedDependencyName = 'very_good_test_runner';
-            const error = 'error';
-            when(() => pubLicense.getLicense(failedDependencyName))
-                .thenThrow(error);
+            when(() => packageConfig.packages).thenReturn({});
 
+            final targetPath = tempDirectory.path;
             final result = await commandRunner.run(
               [
                 ...commandArguments,
                 ignoreRetrievalFailuresArgument,
-                tempDirectory.path,
+                targetPath,
               ],
             );
 
-            const errorMessage =
-                '''\n[$failedDependencyName] Unexpected failure with error: $error''';
+            final errorMessage =
+                '''\n[${veryGoodTestRunnerConfigPackage.name}] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.''';
             verify(() => logger.err(errorMessage)).called(1);
 
             verify(
               () => progress.update(
-                'Collecting licenses from 1 out of 2 packages',
+                'Collecting licenses from 1 out of 1 package',
               ),
             ).called(1);
+            verify(
+              () => progress.complete(
+                '''Retrieved 1 license from 1 package of type: unknown (1).''',
+              ),
+            ).called(1);
+
+            expect(result, equals(ExitCode.success.code));
+          }),
+        );
+
+        test(
+          'when cached package directory cannot be found',
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+            File(path.join(tempDirectory.path, pubspecLockBasename))
+                .writeAsStringSync(_validPubspecLockContent);
+
+            when(() => logger.progress(any())).thenReturn(progress);
+
+            when(() => packageConfig.packages)
+                .thenReturn({veryGoodTestRunnerConfigPackage});
+
+            final packagePath =
+                path.join(tempDirectory.path, 'inexistent', 'nothing');
+            when(() => veryGoodTestRunnerConfigPackage.root).thenReturn(
+              Uri.parse(packagePath),
+            );
+
+            final targetPath = tempDirectory.path;
+            final result = await commandRunner.run(
+              [
+                ...commandArguments,
+                ignoreRetrievalFailuresArgument,
+                targetPath,
+              ],
+            );
+
+            final errorMessage =
+                '''\n[${veryGoodTestRunnerConfigPackage.name}] Could not find package directory at $packagePath.''';
+            verify(() => logger.err(errorMessage)).called(1);
+
             verify(
               () => progress.update(
-                'Collecting licenses from 2 out of 2 packages',
+                'Collecting licenses from 1 out of 1 package',
               ),
             ).called(1);
             verify(
               () => progress.complete(
-                'Retrieved 1 license from 2 packages of type: MIT (1).',
+                '''Retrieved 1 license from 1 package of type: unknown (1).''',
               ),
             ).called(1);
 
             expect(result, equals(ExitCode.success.code));
           }),
         );
-      });
 
-      test(
-        'when all licenses fail to be retrieved',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
-          File(path.join(tempDirectory.path, pubspecLockBasename))
-              .writeAsStringSync(_validMultiplePubspecLockContent);
+        test(
+          'when all licenses fail to be retrieved',
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+            File(path.join(tempDirectory.path, pubspecLockBasename))
+                .writeAsStringSync(_validMultiplePubspecLockContent);
 
-          when(() => logger.progress(any())).thenReturn(progress);
+            when(() => logger.progress(any())).thenReturn(progress);
 
-          const error = 'error';
-          when(() => pubLicense.getLicense(any())).thenThrow(error);
+            const error = 'error';
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            detectLicenseOverride = (name, __) async {
+              // ignore: only_throw_errors
+              throw error;
+            };
 
-          final result = await commandRunner.run(
-            [
-              ...commandArguments,
-              ignoreRetrievalFailuresArgument,
-              tempDirectory.path,
-            ],
-          );
+            final result = await commandRunner.run(
+              [
+                ...commandArguments,
+                ignoreRetrievalFailuresArgument,
+                tempDirectory.path,
+              ],
+            );
 
-          final packageNames = verify(() => pubLicense.getLicense(captureAny()))
-              .captured
-              .cast<String>();
+            final packageNames = packageConfig.packages.map((package) {
+              return package.name;
+            }).toList();
 
-          verify(
-            () => logger.err(
-              '''\n[${packageNames[0]}] Unexpected failure with error: $error''',
-            ),
-          ).called(1);
-          verify(
-            () => logger.err(
-              '''\n[${packageNames[1]}] Unexpected failure with error: $error''',
-            ),
-          ).called(1);
+            final firstPackageName = packageNames[0];
+            final firstPackagePath =
+                path.join(tempDirectory.path, firstPackageName);
+            verify(
+              () => logger.err(
+                '''\n[$firstPackageName] Failed to detect license from $firstPackagePath: $error''',
+              ),
+            ).called(1);
 
-          verify(
-            () => progress.update(
-              'Collecting licenses from 1 out of 2 packages',
-            ),
-          ).called(1);
-          verify(
-            () => progress.update(
-              'Collecting licenses from 2 out of 2 packages',
-            ),
-          ).called(1);
-          verify(
-            () => progress.complete(
-              'Retrieved 0 licenses from 2 packages.',
-            ),
-          ).called(1);
+            final secondPackageName = packageNames[1];
+            final secondPackagePath =
+                path.join(tempDirectory.path, secondPackageName);
+            verify(
+              () => logger.err(
+                '''\n[$secondPackageName] Failed to detect license from $secondPackagePath: $error''',
+              ),
+            ).called(1);
 
-          expect(result, equals(ExitCode.success.code));
-        }),
-      );
+            verify(
+              () => progress.update(
+                'Collecting licenses from 1 out of 2 packages',
+              ),
+            ).called(1);
+            verify(
+              () => progress.update(
+                'Collecting licenses from 2 out of 2 packages',
+              ),
+            ).called(1);
+            verify(
+              () => progress.complete(
+                '''Retrieved 2 licenses from 2 packages of type: unknown (2).''',
+              ),
+            ).called(1);
+
+            expect(result, equals(ExitCode.success.code));
+          }),
+        );
+      });
     });
 
     group('dependency-type', () {
@@ -366,8 +538,7 @@ void main() {
       group('throws usage exception', () {
         test(
           'when no option is provided',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             final result = await commandRunner.run(
               [...commandArguments, dependencyTypeArgument],
             );
@@ -377,8 +548,7 @@ void main() {
 
         test(
           'when invalid option is provided',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             final result = await commandRunner.run(
               [...commandArguments, dependencyTypeArgument, 'invalid'],
             );
@@ -401,22 +571,25 @@ void main() {
                 commandRunner,
                 logger,
                 pubUpdater,
-                pubLicense,
                 printLogs,
               ) async {
                 File(path.join(tempDirectory.path, pubspecLockBasename))
                     .writeAsStringSync(_validPubspecLockContent);
 
+                when(() => packageConfig.packages)
+                    .thenReturn({veryGoodTestRunnerConfigPackage});
+                when(() => detectorResult.matches)
+                    .thenReturn([mitLicenseMatch]);
+
                 when(() => logger.progress(any())).thenReturn(progress);
 
                 final result = await commandRunner.run(
                   [...commandArguments, tempDirectory.path],
                 );
 
-                final packageNames =
-                    verify(() => pubLicense.getLicense(captureAny()))
-                        .captured
-                        .cast<String>();
+                final packageNames = packageConfig.packages.map((package) {
+                  return package.name;
+                }).toList();
 
                 expect(
                   packageNames,
@@ -444,12 +617,16 @@ void main() {
                 commandRunner,
                 logger,
                 pubUpdater,
-                pubLicense,
                 printLogs,
               ) async {
                 File(path.join(tempDirectory.path, pubspecLockBasename))
                     .writeAsStringSync(_validPubspecLockContent);
 
+                when(() => packageConfig.packages)
+                    .thenReturn({veryGoodTestRunnerConfigPackage});
+                when(() => detectorResult.matches)
+                    .thenReturn([mitLicenseMatch]);
+
                 when(() => logger.progress(any())).thenReturn(progress);
 
                 final result = await commandRunner.run(
@@ -461,10 +638,9 @@ void main() {
                   ],
                 );
 
-                final packageNames =
-                    verify(() => pubLicense.getLicense(captureAny()))
-                        .captured
-                        .cast<String>();
+                final packageNames = packageConfig.packages.map((package) {
+                  return package.name;
+                }).toList();
 
                 expect(
                   packageNames,
@@ -493,12 +669,15 @@ void main() {
               commandRunner,
               logger,
               pubUpdater,
-              pubLicense,
               printLogs,
             ) async {
               File(path.join(tempDirectory.path, pubspecLockBasename))
                   .writeAsStringSync(_validPubspecLockContent);
 
+              when(() => packageConfig.packages)
+                  .thenReturn({veryGoodAnalysisConfigPackage});
+              when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
               when(() => logger.progress(any())).thenReturn(progress);
 
               final result = await commandRunner.run(
@@ -510,10 +689,9 @@ void main() {
                 ],
               );
 
-              final packageNames =
-                  verify(() => pubLicense.getLicense(captureAny()))
-                      .captured
-                      .cast<String>();
+              final packageNames = packageConfig.packages.map((package) {
+                return package.name;
+              }).toList();
 
               expect(
                 packageNames,
@@ -541,12 +719,15 @@ void main() {
               commandRunner,
               logger,
               pubUpdater,
-              pubLicense,
               printLogs,
             ) async {
               File(path.join(tempDirectory.path, pubspecLockBasename))
                   .writeAsStringSync(_validPubspecLockContent);
 
+              when(() => packageConfig.packages)
+                  .thenReturn({yamlConfigPackage});
+              when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
               when(() => logger.progress(any())).thenReturn(progress);
 
               final result = await commandRunner.run(
@@ -558,10 +739,9 @@ void main() {
                 ],
               );
 
-              final packageNames =
-                  verify(() => pubLicense.getLicense(captureAny()))
-                      .captured
-                      .cast<String>();
+              final packageNames = packageConfig.packages.map((package) {
+                return package.name;
+              }).toList();
 
               expect(
                 packageNames,
@@ -589,12 +769,18 @@ void main() {
               commandRunner,
               logger,
               pubUpdater,
-              pubLicense,
               printLogs,
             ) async {
               File(path.join(tempDirectory.path, pubspecLockBasename))
                   .writeAsStringSync(_validPubspecLockContent);
 
+              when(() => packageConfig.packages).thenReturn({
+                veryGoodTestRunnerConfigPackage,
+                veryGoodAnalysisConfigPackage,
+                yamlConfigPackage,
+              });
+              when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
               when(() => logger.progress(any())).thenReturn(progress);
 
               final result = await commandRunner.run(
@@ -610,10 +796,9 @@ void main() {
                 ],
               );
 
-              final packageNames =
-                  verify(() => pubLicense.getLicense(captureAny()))
-                      .captured
-                      .cast<String>();
+              final packageNames = packageConfig.packages.map((package) {
+                return package.name;
+              }).toList();
 
               expect(
                 packageNames,
@@ -651,11 +836,14 @@ void main() {
     group('allowed', () {
       test(
         'warns when a license is not recognized',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_validPubspecLockContent);
 
+          when(() => packageConfig.packages)
+              .thenReturn({veryGoodTestRunnerConfigPackage});
+          when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
           when(() => logger.progress(any())).thenReturn(progress);
 
           const invalidLicense = 'not_a_valid_license';
@@ -682,15 +870,15 @@ void main() {
 
       test(
         'exits when a license is not allowed',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_validPubspecLockContent);
 
           when(() => logger.progress(any())).thenReturn(progress);
 
-          when(() => pubLicense.getLicense(any()))
-              .thenAnswer((_) => Future.value({'MIT'}));
+          when(() => packageConfig.packages)
+              .thenReturn({veryGoodTestRunnerConfigPackage});
+          when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
 
           final result = await commandRunner.run(
             [
@@ -708,25 +896,32 @@ void main() {
       group('reports', () {
         test(
           'when a single license is not allowed',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
-            const dependency1Name = 'very_good_test_runner';
-            when(() => pubLicense.getLicense(dependency1Name))
-                .thenAnswer((_) => Future.value({'MIT'}));
-            final license1LinkedMessage = link(
-              uri: pubLicenseUri(dependency1Name),
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            detectLicenseOverride = (String name, _) async {
+              final detectorResult = _MockResult();
+              final licenseMatch = name == veryGoodTestRunnerConfigPackage.name
+                  ? [mitLicenseMatch]
+                  : [bsdLicenseMatch];
+
+              when(() => detectorResult.matches).thenReturn(licenseMatch);
+              return detectorResult;
+            };
+
+            const forbiddenDependencyName = 'very_good_test_runner';
+            final forbiddenDependencyLinkedMessage = link(
+              uri: pubLicenseUri(forbiddenDependencyName),
               message: 'MIT',
             );
 
-            const dependency2Name = 'cli_completion';
-            when(() => pubLicense.getLicense(dependency2Name))
-                .thenAnswer((_) => Future.value({'BSD'}));
-
             await commandRunner.run(
               [
                 ...commandArguments,
@@ -737,7 +932,7 @@ void main() {
             );
 
             final errorMessage =
-                '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).''';
+                '''1 dependency has a banned license: $forbiddenDependencyName ($forbiddenDependencyLinkedMessage).''';
 
             verify(
               () => logger.err(errorMessage),
@@ -747,25 +942,32 @@ void main() {
 
         test(
           'when a single license is not allowed and forbidden is left empty',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
-            const dependency1Name = 'very_good_test_runner';
-            when(() => pubLicense.getLicense(dependency1Name))
-                .thenAnswer((_) => Future.value({'MIT'}));
-            final license1LinkedMessage = link(
-              uri: pubLicenseUri(dependency1Name),
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            detectLicenseOverride = (String name, _) async {
+              final detectorResult = _MockResult();
+              final licenseMatch = name == veryGoodTestRunnerConfigPackage.name
+                  ? [mitLicenseMatch]
+                  : [bsdLicenseMatch];
+
+              when(() => detectorResult.matches).thenReturn(licenseMatch);
+              return detectorResult;
+            };
+
+            const forbiddenDependencyName = 'very_good_test_runner';
+            final forbiddenDependencyLinkedMessage = link(
+              uri: pubLicenseUri(forbiddenDependencyName),
               message: 'MIT',
             );
 
-            const dependency2Name = 'cli_completion';
-            when(() => pubLicense.getLicense(dependency2Name))
-                .thenAnswer((_) => Future.value({'BSD'}));
-
             await commandRunner.run(
               [
                 ...commandArguments,
@@ -778,7 +980,7 @@ void main() {
             );
 
             final errorMessage =
-                '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).''';
+                '''1 dependency has a banned license: $forbiddenDependencyName ($forbiddenDependencyLinkedMessage).''';
 
             verify(
               () => logger.err(errorMessage),
@@ -788,24 +990,33 @@ void main() {
 
         test(
           'when multiple licenses are not allowed',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            detectLicenseOverride = (String name, _) async {
+              final detectorResult = _MockResult();
+              final licenseMatch = name == veryGoodTestRunnerConfigPackage.name
+                  ? [mitLicenseMatch]
+                  : [bsdLicenseMatch];
+
+              when(() => detectorResult.matches).thenReturn(licenseMatch);
+              return detectorResult;
+            };
+
             const dependency1Name = 'very_good_test_runner';
-            when(() => pubLicense.getLicense(dependency1Name))
-                .thenAnswer((_) => Future.value({'MIT'}));
             final license1LinkedMessage = link(
               uri: pubLicenseUri(dependency1Name),
               message: 'MIT',
             );
 
             const dependency2Name = 'cli_completion';
-            when(() => pubLicense.getLicense(dependency2Name))
-                .thenAnswer((_) => Future.value({'BSD'}));
             final license2LinkedMessage = link(
               uri: pubLicenseUri(dependency2Name),
               message: 'BSD',
@@ -834,13 +1045,16 @@ void main() {
     group('forbidden', () {
       test(
         'warns when a license is not recognized',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_validPubspecLockContent);
 
           when(() => logger.progress(any())).thenReturn(progress);
 
+          when(() => packageConfig.packages)
+              .thenReturn({veryGoodTestRunnerConfigPackage});
+          when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
           const invalidLicense = 'not_a_valid_license';
           await commandRunner.run(
             [
@@ -865,15 +1079,15 @@ void main() {
 
       test(
         'exits when a license is forbidden',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_validPubspecLockContent);
 
           when(() => logger.progress(any())).thenReturn(progress);
 
-          when(() => pubLicense.getLicense(any()))
-              .thenAnswer((_) => Future.value({'BSD'}));
+          when(() => packageConfig.packages)
+              .thenReturn({veryGoodTestRunnerConfigPackage});
+          when(() => detectorResult.matches).thenReturn([bsdLicenseMatch]);
 
           final result = await commandRunner.run(
             [
@@ -891,25 +1105,34 @@ void main() {
       group('report', () {
         test(
           'when a single license is forbidden',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
-            const dependency1Name = 'very_good_test_runner';
-            when(() => pubLicense.getLicense(dependency1Name))
-                .thenAnswer((_) => Future.value({'MIT'}));
-            final license1LinkedMessage = link(
-              uri: pubLicenseUri(dependency1Name),
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            final packageLicenseMatch = {
+              veryGoodTestRunnerConfigPackage.name: [mitLicenseMatch],
+              cliCompletionConfigPackage.name: [bsdLicenseMatch],
+            };
+            detectLicenseOverride = (String name, _) async {
+              final detectorResult = _MockResult();
+              final licenseMatch = packageLicenseMatch[name]!;
+
+              when(() => detectorResult.matches).thenReturn(licenseMatch);
+              return detectorResult;
+            };
+
+            const forbiddenLicenseName = 'very_good_test_runner';
+            final forbiddenLicenseLinkMessage = link(
+              uri: pubLicenseUri(forbiddenLicenseName),
               message: 'MIT',
             );
 
-            const dependency2Name = 'cli_completion';
-            when(() => pubLicense.getLicense(dependency2Name))
-                .thenAnswer((_) => Future.value({'BSD'}));
-
             await commandRunner.run(
               [
                 ...commandArguments,
@@ -920,7 +1143,7 @@ void main() {
             );
 
             final errorMessage =
-                '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).''';
+                '''1 dependency has a banned license: $forbiddenLicenseName ($forbiddenLicenseLinkMessage).''';
 
             verify(
               () => logger.err(errorMessage),
@@ -930,25 +1153,34 @@ void main() {
 
         test(
           'when a single license is forbidden and allowed is left empty',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
-            const dependency1Name = 'very_good_test_runner';
-            when(() => pubLicense.getLicense(dependency1Name))
-                .thenAnswer((_) => Future.value({'MIT'}));
-            final license1LinkedMessage = link(
-              uri: pubLicenseUri(dependency1Name),
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            final packageLicenseMatch = {
+              veryGoodTestRunnerConfigPackage.name: [mitLicenseMatch],
+              cliCompletionConfigPackage.name: [bsdLicenseMatch],
+            };
+            detectLicenseOverride = (String name, _) async {
+              final detectorResult = _MockResult();
+              final licenseMatch = packageLicenseMatch[name]!;
+
+              when(() => detectorResult.matches).thenReturn(licenseMatch);
+              return detectorResult;
+            };
+
+            const forbiddenLicenseName = 'very_good_test_runner';
+            final forbiddenLicenseLinkMessage = link(
+              uri: pubLicenseUri(forbiddenLicenseName),
               message: 'MIT',
             );
 
-            const dependency2Name = 'cli_completion';
-            when(() => pubLicense.getLicense(dependency2Name))
-                .thenAnswer((_) => Future.value({'BSD'}));
-
             await commandRunner.run(
               [
                 ...commandArguments,
@@ -961,7 +1193,7 @@ void main() {
             );
 
             final errorMessage =
-                '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).''';
+                '''1 dependency has a banned license: $forbiddenLicenseName ($forbiddenLicenseLinkMessage).''';
 
             verify(
               () => logger.err(errorMessage),
@@ -971,24 +1203,35 @@ void main() {
 
         test(
           'when multiple licenses are forbidden',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            final packageLicenseMatch = {
+              veryGoodTestRunnerConfigPackage.name: [mitLicenseMatch],
+              cliCompletionConfigPackage.name: [bsdLicenseMatch],
+            };
+            detectLicenseOverride = (String name, _) async {
+              final detectorResult = _MockResult();
+              final licenseMatch = packageLicenseMatch[name]!;
+
+              when(() => detectorResult.matches).thenReturn(licenseMatch);
+              return detectorResult;
+            };
+
             const dependency1Name = 'very_good_test_runner';
-            when(() => pubLicense.getLicense(dependency1Name))
-                .thenAnswer((_) => Future.value({'MIT'}));
             final license1LinkedMessage = link(
               uri: pubLicenseUri(dependency1Name),
               message: 'MIT',
             );
 
             const dependency2Name = 'cli_completion';
-            when(() => pubLicense.getLicense(dependency2Name))
-                .thenAnswer((_) => Future.value({'BSD'}));
             final license2LinkedMessage = link(
               uri: pubLicenseUri(dependency2Name),
               message: 'BSD',
@@ -1022,13 +1265,18 @@ void main() {
       group('skips', () {
         test(
           'a single package by name',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
             when(() => logger.progress(any())).thenReturn(progress);
 
+            when(() => packageConfig.packages).thenReturn({
+              veryGoodTestRunnerConfigPackage,
+              cliCompletionConfigPackage,
+            });
+            when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
+
             final result = await commandRunner.run(
               [
                 ...commandArguments,
@@ -1054,8 +1302,7 @@ void main() {
 
         test(
           'multiple packages by name',
-          withRunner(
-              (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+          withRunner((commandRunner, logger, pubUpdater, printLogs) async {
             File(path.join(tempDirectory.path, pubspecLockBasename))
                 .writeAsStringSync(_validMultiplePubspecLockContent);
 
@@ -1085,10 +1332,25 @@ void main() {
     });
 
     group('exits with error', () {
+      test(
+        'when target path does not exist',
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+          final targetPath = path.join(tempDirectory.path, 'inexistent');
+          final result = await commandRunner.run(
+            [...commandArguments, targetPath],
+          );
+
+          final errorMessage =
+              '''Could not find directory at $targetPath. Specify a valid path to a Dart or Flutter project.''';
+          verify(() => logger.err(errorMessage)).called(1);
+
+          expect(result, equals(ExitCode.noInput.code));
+        }),
+      );
+
       test(
         'when it did not find a pubspec.lock file at the target path',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           when(() => logger.progress(any())).thenReturn(progress);
 
           final result = await commandRunner.run(
@@ -1107,8 +1369,7 @@ void main() {
 
       test(
         'when it failed to parse a pubspec.lock file at the target path',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync('');
 
@@ -1130,8 +1391,7 @@ void main() {
 
       test(
         'when no dependencies of type are found',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_emptyPubspecLockContent);
 
@@ -1152,65 +1412,119 @@ void main() {
       );
 
       test(
-        'when PubLicense throws a PubLicenseException',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        'when detectLicense throws an unknown error',
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_validPubspecLockContent);
 
-          when(() => logger.progress(any())).thenReturn(progress);
+          when(() => packageConfig.packages).thenReturn({
+            veryGoodTestRunnerConfigPackage,
+          });
 
-          const exception = PubLicenseException('message');
-          when(() => pubLicense.getLicense('very_good_test_runner'))
-              .thenThrow(exception);
+          const error = 'error';
+          // ignore: only_throw_errors
+          detectLicenseOverride = (_, __) => throw error;
+
+          when(() => logger.progress(any())).thenReturn(progress);
 
           final result = await commandRunner.run(
             [...commandArguments, tempDirectory.path],
           );
 
-          final packageName = verify(() => pubLicense.getLicense(captureAny()))
-              .captured
-              .cast<String>()
-              .first;
+          final packageName = packageConfig.packages.first.name;
+          final packagePath = path.join(tempDirectory.path, packageName);
+          final errorMessage =
+              '''[$packageName] Failed to detect license from $packagePath: $error''';
 
-          final errorMessage = '[$packageName] ${exception.message}';
           verify(() => logger.err(errorMessage)).called(1);
 
           verify(() => progress.cancel()).called(1);
 
-          expect(result, equals(ExitCode.unavailable.code));
+          expect(result, equals(ExitCode.software.code));
         }),
       );
 
       test(
-        'when PubLicense throws an unknown error',
-        withRunner(
-            (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+        'when there is no package config file',
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
           File(path.join(tempDirectory.path, pubspecLockBasename))
               .writeAsStringSync(_validPubspecLockContent);
 
+          when(() => logger.progress(any())).thenReturn(progress);
+
           const error = 'error';
-          when(() => pubLicense.getLicense('very_good_test_runner'))
-              .thenThrow(error);
+          // ignore: only_throw_errors
+          findPackageConfigOverride = (_) async => throw error;
+
+          final targetPath = tempDirectory.path;
+          final result = await commandRunner.run(
+            [...commandArguments, targetPath],
+          );
+
+          final errorMessage =
+              '''Could not find a valid package config in $targetPath. Run `dart pub get` or `flutter pub get` to generate one.''';
+          verify(() => logger.err(errorMessage)).called(1);
+
+          verify(() => progress.cancel()).called(1);
+
+          expect(result, equals(ExitCode.noInput.code));
+        }),
+      );
+
+      test(
+        'when cached package path cannot be found',
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+          File(path.join(tempDirectory.path, pubspecLockBasename))
+              .writeAsStringSync(_validPubspecLockContent);
 
           when(() => logger.progress(any())).thenReturn(progress);
 
+          when(() => packageConfig.packages).thenReturn({});
+
+          final targetPath = tempDirectory.path;
           final result = await commandRunner.run(
-            [...commandArguments, tempDirectory.path],
+            [...commandArguments, targetPath],
           );
 
-          final packageName = verify(() => pubLicense.getLicense(captureAny()))
-              .captured
-              .cast<String>()
-              .first;
+          final errorMessage =
+              '''[${veryGoodTestRunnerConfigPackage.name}] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.''';
+          verify(() => logger.err(errorMessage)).called(1);
+
+          verify(() => progress.cancel()).called(1);
+
+          expect(result, equals(ExitCode.noInput.code));
+        }),
+      );
+
+      test(
+        'when cached package directory cannot be found',
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+          File(path.join(tempDirectory.path, pubspecLockBasename))
+              .writeAsStringSync(_validPubspecLockContent);
+
+          when(() => logger.progress(any())).thenReturn(progress);
+
+          when(() => packageConfig.packages)
+              .thenReturn({veryGoodTestRunnerConfigPackage});
+
+          final packagePath =
+              path.join(tempDirectory.path, 'inexistent', 'nothing');
+          when(() => veryGoodTestRunnerConfigPackage.root).thenReturn(
+            Uri.parse(packagePath),
+          );
+
+          final targetPath = tempDirectory.path;
+          final result = await commandRunner.run(
+            [...commandArguments, targetPath],
+          );
 
           final errorMessage =
-              '[$packageName] Unexpected failure with error: $error';
+              '''[${veryGoodTestRunnerConfigPackage.name}] Could not find package directory at $packagePath.''';
           verify(() => logger.err(errorMessage)).called(1);
 
           verify(() => progress.cancel()).called(1);
 
-          expect(result, equals(ExitCode.software.code));
+          expect(result, equals(ExitCode.noInput.code));
         }),
       );
     });
diff --git a/test/src/commands/packages/commands/get_test.dart b/test/src/commands/packages/commands/get_test.dart
index 79e3f9e..47072b1 100644
--- a/test/src/commands/packages/commands/get_test.dart
+++ b/test/src/commands/packages/commands/get_test.dart
@@ -22,8 +22,7 @@ void main() {
   group('packages get', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(['packages', 'get', '--help']);
         expect(printLogs, equals(_expectedPackagesGetUsage));
         expect(result, equals(ExitCode.success.code));
@@ -39,8 +38,7 @@ void main() {
     test(
       'throws usage exception '
       'when too many arguments are provided',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(
           ['packages', 'get', 'arg1', 'arg2'],
         );
@@ -51,8 +49,7 @@ void main() {
     test(
       'throws pubspec not found exception '
       'when no pubspec.yaml exists',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(['packages', 'get', 'test']);
         expect(result, equals(ExitCode.noInput.code));
         verify(() {
@@ -64,8 +61,7 @@ void main() {
     test(
       'throws pubspec not found exception '
       'when no pubspec.yaml exists (recursive)',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(
           ['packages', 'get', '-r', 'site'],
         );
@@ -78,8 +74,7 @@ void main() {
 
     test(
       'throws when installation fails',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
@@ -94,8 +89,7 @@ void main() {
 
     test(
       'ignores .fvm directory',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
@@ -123,8 +117,7 @@ void main() {
     test(
       'completes normally '
       'when pubspec.yaml exists',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
@@ -152,8 +145,7 @@ void main() {
     test(
       'completes normally '
       'when pubspec.yaml exists (recursive)',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
@@ -201,8 +193,7 @@ void main() {
     test(
       'completes normally '
       'when pubspec.yaml exists and directory is not ignored (recursive)',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
@@ -253,8 +244,7 @@ void main() {
     test(
       'completes normally '
       '''when pubspec.yaml exists and directory is not ignored (recursive) with an empty glob''',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         final directory = Directory(
           path.join(tempDirectory.path, 'macos_plugin'),
@@ -304,8 +294,7 @@ void main() {
     test(
       'completes normally '
       'when pubspec.yaml exists and directory is ignored (recursive)',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
diff --git a/test/src/commands/packages/packages_test.dart b/test/src/commands/packages/packages_test.dart
index 7a83121..e16b292 100644
--- a/test/src/commands/packages/packages_test.dart
+++ b/test/src/commands/packages/packages_test.dart
@@ -21,8 +21,7 @@ void main() {
   group('packages', () {
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(['packages', '--help']);
         expect(printLogs, equals(_expectedPackagesUsage));
         expect(result, equals(ExitCode.success.code));
diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart
index a23a957..046c956 100644
--- a/test/src/commands/test/test_test.dart
+++ b/test/src/commands/test/test_test.dart
@@ -111,8 +111,7 @@ void main() {
 
     test(
       'help',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final result = await commandRunner.run(['test', '--help']);
         expect(printLogs, equals(expectedTestUsage));
         expect(result, equals(ExitCode.success.code));
@@ -128,8 +127,7 @@ void main() {
     test(
       'throws pubspec not found exception '
       'when no pubspec.yaml exists',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
@@ -144,8 +142,7 @@ void main() {
 
     test(
       'completes normally when no pubspec.yaml exists (recursive)',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final tempDirectory = Directory.systemTemp.createTempSync();
         addTearDown(() => tempDirectory.deleteSync(recursive: true));
 
diff --git a/test/src/commands/update_test.dart b/test/src/commands/update_test.dart
index 1c078c1..6bbf271 100644
--- a/test/src/commands/update_test.dart
+++ b/test/src/commands/update_test.dart
@@ -29,8 +29,7 @@ void main() {
 
     test(
       'handles pub latest version query errors',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         when(
           () => pubUpdater.getLatestVersion(any()),
         ).thenThrow(Exception('oops'));
@@ -49,8 +48,7 @@ void main() {
 
     test(
       'handles pub update errors',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         when(
           () => pubUpdater.getLatestVersion(any()),
         ).thenAnswer((_) async => latestVersion);
@@ -75,8 +73,7 @@ void main() {
 
     test(
       'handles pub update process errors',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         final errorProcessResult = ProcessResult(
           42,
           1,
@@ -115,8 +112,7 @@ void main() {
 
     test(
       'updates when newer version exists',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         when(
           () => pubUpdater.getLatestVersion(any()),
         ).thenAnswer((_) async => latestVersion);
@@ -143,8 +139,7 @@ void main() {
 
     test(
       'does not update when already on latest version',
-      withRunner(
-          (commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
         when(
           () => pubUpdater.getLatestVersion(any()),
         ).thenAnswer((_) async => packageVersion);
diff --git a/test/src/pub_license/fixtures/generate_pub_license_fixtures.dart b/test/src/pub_license/fixtures/generate_pub_license_fixtures.dart
deleted file mode 100644
index 476b70f..0000000
--- a/test/src/pub_license/fixtures/generate_pub_license_fixtures.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-/// A small script used to generate the fixture for the pub_license test.
-///
-/// Fixtures are simply a temporary snapshot of an HTML response from pub.dev.
-/// The generated fixtures allow testing pub_license scraping logic without
-/// making a request to pub.dev every time the test is run.
-///
-/// To run this script, use the following command:
-/// ```bash
-/// dart test/src/pub_license/fixtures/generate_pub_license_fixtures.dart
-/// ```
-///
-/// Or simply use the "Run" CodeLens from VSCode's Dart extension if running
-/// from VSCode.
-library generate_pub_license_fixtures;
-
-// ignore_for_file: avoid_print
-
-import 'dart:io';
-
-import 'package:http/http.dart' as http;
-import 'package:path/path.dart' as path;
-
-/// [Uri] used to test the case where a package has a single license.
-final _singleLicenseUri = Uri.parse(
-  'https://pub.dev/packages/very_good_cli/license',
-);
-
-/// [Uri] used to test the case where a package has multiple licenses.
-final _multipleLicenseUri = Uri.parse(
-  'https://pub.dev/packages/just_audio/license',
-);
-
-/// [Uri] used to test the case where a package has no license.
-final _noLicenseUri = Uri.parse(
-  'https://pub.dev/packages/music_control_notification/license',
-);
-
-Future<void> main() async {
-  final fixtureUris = <String, Uri>{
-    'singleLicense': _singleLicenseUri,
-    'multipleLicense': _multipleLicenseUri,
-    'noLicense': _noLicenseUri,
-  };
-
-  final httpClient = http.Client();
-
-  for (final entry in fixtureUris.entries) {
-    final name = entry.key;
-    final uri = entry.value;
-
-    final response = await httpClient.get(uri);
-
-    if (response.statusCode != 200) {
-      print(
-        '''Failed to generate a fixture for $name, received status code: ${response.statusCode}''',
-      );
-      continue;
-    }
-
-    final fixturePath = path.joinAll([
-      Directory.current.path,
-      'test',
-      'src',
-      'pub_license',
-      'fixtures',
-      '$name.html',
-    ]);
-    File(fixturePath)
-      ..createSync(recursive: true)
-      ..writeAsStringSync(response.body);
-
-    print('Fixture generated at $fixturePath');
-  }
-}
diff --git a/test/src/pub_license/fixtures/multipleLicense.html b/test/src/pub_license/fixtures/multipleLicense.html
deleted file mode 100644
index 1b3902b..0000000
--- a/test/src/pub_license/fixtures/multipleLicense.html
+++ /dev/null
@@ -1,231 +0,0 @@
-<!DOCTYPE html>
-<html lang="en-us"><head><script src="https://www.googletagmanager.com/gtm.js?id=GTM-MX6DBN9" async="async"></script><script src="/static/hash-i7rnnc9t/js/gtm.js" async="async"></script><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1"/><meta name="twitter:card" content="summary"/><meta name="twitter:site" content="@dart_lang"/><meta name="twitter:description" content="A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset/file/URL/stream) in a variety of audio formats with gapless playback."/><meta name="twitter:image" content="https://pub.dev/static/hash-i7rnnc9t/img/pub-dev-icon-cover-image.png"/><meta property="og:type" content="website"/><meta property="og:site_name" content="Dart packages"/><meta property="og:title" content="just_audio | Flutter Package"/><meta property="og:description" content="A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset/file/URL/stream) in a variety of audio formats with gapless playback."/><meta property="og:image" content="https://pub.dev/static/hash-i7rnnc9t/img/pub-dev-icon-cover-image.png"/><meta property="og:url" content="https://pub.dev/packages/just_audio/license"/><title>just_audio | Flutter Package</title><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&amp;family=Google+Sans+Display:wght@400&amp;family=Google+Sans+Text:wght@400;500;700&amp;family=Google+Sans+Mono:wght@400;700&amp;display=swap"/><link rel="shortcut icon" href="/static/hash-i7rnnc9t/img/flutter-logo-32x32.png"/><link rel="stylesheet" href="https://www.gstatic.com/glue/v25_0/ccb.min.css"/><link rel="search" type="application/opensearchdescription+xml" title="Dart packages" href="/osd.xml"/><link rel="canonical" href="https://pub.dev/packages/just_audio/license"/><meta name="description" content="A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset/file/URL/stream) in a variety of audio formats with gapless playback."/><link rel="alternate" type="application/atom+xml" title="Updated Packages Feed for Pub" href="/feed.atom"/><link rel="stylesheet" type="text/css" href="/static/hash-i7rnnc9t/material/bundle/styles.css"/><link rel="stylesheet" type="text/css" href="/static/hash-i7rnnc9t/css/style.css"/><script src="/static/hash-i7rnnc9t/material/bundle/script.min.js" defer="defer"></script><script src="/static/hash-i7rnnc9t/js/script.dart.js" defer="defer"></script><script src="https://www.gstatic.com/brandstudio/kato/cookie_choice_component/cookie_consent_bar.v3.js" defer="defer" data-autoload-cookie-consent-bar="true"></script><meta name="pub-page-data" content="eyJwa2dEYXRhIjp7InBhY2thZ2UiOiJqdXN0X2F1ZGlvIiwidmVyc2lvbiI6IjAuOS4zNSIsImxpa2VzIjoyOTg3LCJwdWJsaXNoZXJJZCI6InJ5YW5oZWlzZS5jb20iLCJpc0Rpc2NvbnRpbnVlZCI6ZmFsc2V9LCJzZXNzaW9uQXdhcmUiOmZhbHNlfQ=="/><link rel="preload" href="/static/hash-i7rnnc9t/highlight/highlight-with-init.js" as="script"/></head><body class="light-theme"><noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MX6DBN9" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript><div class="site-header"><button class="hamburger" aria-label="menu toggle"></button><a class="logo" href="/"><img class="site-logo" src="/static/hash-i7rnnc9t/img/pub-dev-logo.svg" alt="pub logo" width="140" height="30"/></a><div class="site-header-space"></div><div class="site-header-mask"></div><div class="site-header-search"><form action="/packages" method="GET"><input class="site-header-search-input" name="q" placeholder="New search..." autocomplete="on"/></form></div><div class="site-header-nav scroll-container"><div class="nav-login-container"><button id="-account-login" class="nav-main-button link">Sign in</button></div><div class="nav-container nav-help-container hoverable"><button class="nav-main-button">Help</button><div class="nav-hover-popup"><div class="nav-table-columns"><div class="nav-table-column"><h3>Pub.dev</h3><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div><div class="nav-table-column"><h3>Flutter</h3><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div><div class="nav-table-column"><h3>Dart</h3><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Pub.dev <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Flutter <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Dart <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div id="banner-container"></div><main class="container"><div class="detail-wrapper -active -has-info-box"><div class="detail-banners"><a href="https://flutter.dev/docs/development/packages-and-plugins/favorites" rel="noopener" target="_blank"><img class="ff-banner ff-banner-desktop" src="/static/hash-i7rnnc9t/img/ff-banner-desktop-2x.png" alt="large Flutter Favorite logo" width="150" height="218" title="Package is a Flutter Favorite"/><img class="ff-banner ff-banner-mobile" src="/static/hash-i7rnnc9t/img/ff-banner-mobile-2x.png" alt="small Flutter Favorite logo" width="94" height="116" title="Package is a Flutter Favorite"/></a></div><div class="detail-header -is-loose"><div class="detail-container"><div class="detail-header-outer-block"><div class="detail-header-content-block"><h1 class="title">just_audio 0.9.35 <span class="pkg-page-title-copy"><img class="pkg-page-title-copy-icon" src="/static/hash-i7rnnc9t/img/content-copy-icon.svg" alt="icon indicating copy to clipboard operation" width="18" height="18" title="Copy &quot;just_audio: ^0.9.35&quot; to clipboard" data-copy-content="just_audio: ^0.9.35" data-ga-click-event="copy-package-version"/><div class="pkg-page-title-copy-feedback"><span class="code">just_audio: ^0.9.35</span> copied to clipboard</div></span></h1><div class="metadata">Published <span><a class="-x-ago" href="" title="Sep 12, 2023" aria-label="Switch between date and elapsed time." aria-role="button">17 days ago</a></span> • <a class="-pub-publisher" href="/publishers/ryanheise.com"><img class="-pub-publisher-shield" src="/static/hash-i7rnnc9t/img/material-icon-verified.svg" alt="verified publisher icon" width="14" height="14" title="Published by a pub.dev verified publisher"/>ryanheise.com</a><span class="package-badge" title="Package is compatible with Dart 3.">Dart 3 compatible</span></div><div class="detail-tags-and-like"><div class="detail-tags"><div class="-pub-tag-badge"><span class="tag-badge-main">SDK</span><a class="tag-badge-sub" href="/packages?q=sdk%3Aflutter" rel="nofollow" title="Packages compatible with Flutter SDK">Flutter</a></div><div class="-pub-tag-badge"><span class="tag-badge-main">Platform</span><a class="tag-badge-sub" href="/packages?q=platform%3Aandroid" rel="nofollow" title="Packages compatible with Android platform">Android</a><a class="tag-badge-sub" href="/packages?q=platform%3Aios" rel="nofollow" title="Packages compatible with iOS platform">iOS</a><a class="tag-badge-sub" href="/packages?q=platform%3Amacos" rel="nofollow" title="Packages compatible with macOS platform">macOS</a><a class="tag-badge-sub" href="/packages?q=platform%3Aweb" rel="nofollow" title="Packages compatible with Web platform">web</a></div></div><div class="detail-like"><button id="-pub-like-icon-button" class="mdc-icon-button" aria-label="Like this package" data-ga-click-event="toggle-like" aria-pressed="false"><img class="mdc-icon-button__icon" src="/static/hash-i7rnnc9t/img/like-inactive.svg" alt="icon indicating liked status (inactive)" width="18" height="18"/><img class="mdc-icon-button__icon mdc-icon-button__icon--on" src="/static/hash-i7rnnc9t/img/like-active.svg" alt="icon indicating liked status (active)" width="18" height="18"/></button><span class="likes-count"><span id="likes-count">2.9k</span></span></div></div></div></div></div></div><div class="detail-container"><div class="detail-lead"><div class="detail-metadata-toggle"><div class="detail-metadata-toggle-icon">→</div><h3 class="detail-lead-title">Metadata</h3></div><p class="detail-lead-text">A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset&#47;file&#47;URL&#47;stream) in a variety of audio formats with gapless playback.</p><p class="detail-lead-more"><a class="detail-metadata-toggle">More...</a></p></div></div><div class="detail-body"><div class="detail-tabs"><div class="detail-tabs-wide-header"><div class="detail-container"><ul class="detail-tabs-header"><li class="detail-tab tab-link detail-tab-readme-title" role="button"><a href="/packages/just_audio">Readme</a></li><li class="detail-tab tab-link detail-tab-changelog-title" role="button"><a href="/packages/just_audio/changelog">Changelog</a></li><li class="detail-tab tab-link detail-tab-example-title" role="button"><a href="/packages/just_audio/example">Example</a></li><li class="detail-tab tab-link detail-tab-installing-title" role="button"><a href="/packages/just_audio/install">Installing</a></li><li class="detail-tab tab-button detail-tab-license-title -active" role="button">License</li><li class="detail-tab tab-link detail-tab-versions-title" role="button"><a href="/packages/just_audio/versions">Versions</a></li><li class="detail-tab tab-link detail-tab-analysis-title" role="button"><a href="/packages/just_audio/score">Scores</a></li></ul></div></div><div class="detail-container detail-body-main"><div class="detail-tabs-content"><section class="tab-content detail-tab-license-content -active markdown-body"><h2>License</h2><div class="highlight"><pre>MIT License
-
-Copyright (c) 2019-2020 Ryan Heise and the project contributors.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the &quot;Software&quot;), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and&#47;or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-==============================================================================
-
-This software includes the ExoPlayer library which is licensed under the Apache
-License, Version 2.0.
-
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http:&#47;&#47;www.apache.org&#47;licenses&#47;
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      &quot;License&quot; shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      &quot;Licensor&quot; shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      &quot;Legal Entity&quot; shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      &quot;control&quot; means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      &quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      &quot;Source&quot; form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      &quot;Object&quot; form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      &quot;Work&quot; shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      &quot;Derivative Works&quot; shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      &quot;Contribution&quot; shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, &quot;submitted&quot;
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as &quot;Not a Contribution.&quot;
-
-      &quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a &quot;NOTICE&quot; text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and&#47;or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets &quot;[]&quot;
-      replaced with your own identifying information. (Don&#39;t include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same &quot;printed page&quot; as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http:&#47;&#47;www.apache.org&#47;licenses&#47;LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-</pre></div></section></div></div></div><aside class="detail-info-box"><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div><a class="packages-scores" href="/packages/just_audio/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">2987</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">140</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">pub points</div></div><div class="packages-score packages-score-popularity"><div class="packages-score-value -has-value"><span class="packages-score-value-number">99</span><span class="packages-score-value-sign">%</span></div><div class="packages-score-label">popularity</div></div></a><h3 class="title">Publisher</h3><p><a href="/publishers/ryanheise.com"><img class="-pub-publisher-shield" src="/static/hash-i7rnnc9t/img/material-icon-verified.svg" alt="verified publisher icon" width="14" height="14" title="Published by a pub.dev verified publisher"/>ryanheise.com</a></p><h3 class="title pkg-infobox-metadata">Metadata</h3><p>A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset&#47;file&#47;URL&#47;stream) in a variety of audio formats with gapless playback.</p><p><a class="link" href="https://github.com/ryanheise/just_audio/tree/minor/just_audio" rel="ugc">Repository (GitHub)</a><br/><a class="link" href="https://github.com/ryanheise/just_audio/issues" rel="ugc">View&#47;report issues</a><br/><a class="link" href="https://github.com/ryanheise/just_audio/blob/minor/CONTRIBUTING.md" rel="ugc">Contributing</a><br/></p><h3 class="title">Topics</h3><p><a href="/packages?q=topic%3Aaudio" rel="nofollow">#audio</a> <a href="/packages?q=topic%3Asound" rel="nofollow">#sound</a> <a href="/packages?q=topic%3Aplayer" rel="nofollow">#player</a> <a href="/packages?q=topic%3Abackground" rel="nofollow">#background</a></p><h3 class="title">Documentation</h3><p><a class="link" href="/documentation/just_audio/latest/">API reference</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img" src="/static/hash-i7rnnc9t/img/material-icon-balance.svg" alt="Icon for licenses." width="14" height="14"/>Apache-2.0, MIT (<a href="/packages/just_audio/license">LICENSE</a>)</p><h3 class="title">Dependencies</h3><p><a href="/packages/async" title="^2.5.0">async</a>, <a href="/packages/audio_session" title="^0.1.14">audio_session</a>, <a href="/packages/crypto" title="^3.0.0">crypto</a>, <a href="https://api.flutter.dev/">flutter</a>, <a href="/packages/just_audio_platform_interface" title="^4.2.1">just_audio_platform_interface</a>, <a href="/packages/just_audio_web" title="^0.4.8">just_audio_web</a>, <a href="/packages/meta" title="^1.3.0">meta</a>, <a href="/packages/path" title="^1.8.0">path</a>, <a href="/packages/path_provider" title="^2.0.0">path_provider</a>, <a href="/packages/rxdart" title="&gt;=0.26.0 &lt;0.28.0">rxdart</a>, <a href="/packages/uuid" title="&gt;=3.0.1 &lt;5.0.0">uuid</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Ajust_audio" rel="nofollow">Packages that depend on just_audio</a></p></aside></div><script type="application/ld+json">{"@context":"http\u003a\u002f\u002fschema.org","@type":"SoftwareSourceCode","name":"just\u005faudio","version":"0.9.35","description":"just\u005faudio - A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source \u0028asset\u002ffile\u002fURL\u002fstream\u0029 in a variety of audio formats with gapless playback.","url":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002fjust\u005faudio","dateCreated":"2019-11-28T06\u003a58\u003a54.784417Z","dateModified":"2023-09-12T07\u003a00\u003a03.788376Z","programmingLanguage":"Dart","image":"https\u003a\u002f\u002fpub.dev\u002fstatic\u002fimg\u002fpub-dev-icon-cover-image.png","license":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002fjust\u005faudio\u002flicense"}</script></div><div class="detail-metadata"><h3 class="detail-metadata-title"><span class="detail-metadata-toggle">←</span> Metadata</h3><div class="detail-info-box"><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div><a class="packages-scores" href="/packages/just_audio/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">2987</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">140</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">pub points</div></div><div class="packages-score packages-score-popularity"><div class="packages-score-value -has-value"><span class="packages-score-value-number">99</span><span class="packages-score-value-sign">%</span></div><div class="packages-score-label">popularity</div></div></a><h3 class="title">Publisher</h3><p><a href="/publishers/ryanheise.com"><img class="-pub-publisher-shield" src="/static/hash-i7rnnc9t/img/material-icon-verified.svg" alt="verified publisher icon" width="14" height="14" title="Published by a pub.dev verified publisher"/>ryanheise.com</a></p><h3 class="title pkg-infobox-metadata">Metadata</h3><p>A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset&#47;file&#47;URL&#47;stream) in a variety of audio formats with gapless playback.</p><p><a class="link" href="https://github.com/ryanheise/just_audio/tree/minor/just_audio" rel="ugc">Repository (GitHub)</a><br/><a class="link" href="https://github.com/ryanheise/just_audio/issues" rel="ugc">View&#47;report issues</a><br/><a class="link" href="https://github.com/ryanheise/just_audio/blob/minor/CONTRIBUTING.md" rel="ugc">Contributing</a><br/></p><h3 class="title">Topics</h3><p><a href="/packages?q=topic%3Aaudio" rel="nofollow">#audio</a> <a href="/packages?q=topic%3Asound" rel="nofollow">#sound</a> <a href="/packages?q=topic%3Aplayer" rel="nofollow">#player</a> <a href="/packages?q=topic%3Abackground" rel="nofollow">#background</a></p><h3 class="title">Documentation</h3><p><a class="link" href="/documentation/just_audio/latest/">API reference</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img" src="/static/hash-i7rnnc9t/img/material-icon-balance.svg" alt="Icon for licenses." width="14" height="14"/>Apache-2.0, MIT (<a href="/packages/just_audio/license">LICENSE</a>)</p><h3 class="title">Dependencies</h3><p><a href="/packages/async" title="^2.5.0">async</a>, <a href="/packages/audio_session" title="^0.1.14">audio_session</a>, <a href="/packages/crypto" title="^3.0.0">crypto</a>, <a href="https://api.flutter.dev/">flutter</a>, <a href="/packages/just_audio_platform_interface" title="^4.2.1">just_audio_platform_interface</a>, <a href="/packages/just_audio_web" title="^0.4.8">just_audio_web</a>, <a href="/packages/meta" title="^1.3.0">meta</a>, <a href="/packages/path" title="^1.8.0">path</a>, <a href="/packages/path_provider" title="^2.0.0">path_provider</a>, <a href="/packages/rxdart" title="&gt;=0.26.0 &lt;0.28.0">rxdart</a>, <a href="/packages/uuid" title="&gt;=3.0.1 &lt;5.0.0">uuid</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Ajust_audio" rel="nofollow">Packages that depend on just_audio</a></p></div><p class="detail-lead-back"><a class="detail-metadata-toggle">Back</a></p></div></main><footer class="site-footer"><a class="link" href="https://dart.dev/">Dart language</a><a class="link sep" href="/policy">Policy</a><a class="link sep" href="https://www.google.com/intl/en/policies/terms/">Terms</a><a class="link sep" href="https://developers.google.com/terms/">API Terms</a><a class="link sep" href="/security">Security</a><a class="link sep" href="https://www.google.com/intl/en/policies/privacy/">Privacy</a><a class="link sep" href="/help">Help</a><a class="link icon sep" href="/feed.atom"><img class="inline-icon" src="/static/hash-i7rnnc9t/img/rss-feed-icon.svg" alt="RSS icon" width="20" height="20" title="RSS/atom feed"/></a><a class="link icon github_issue" href="https://github.com/dart-lang/pub-dev/issues/new"><img class="inline-icon" src="/static/hash-i7rnnc9t/img/bug-report-white-96px.png" alt="bug report icon" width="20" height="20" title="Report an issue with this site"/></a></footer><script src="/static/hash-i7rnnc9t/highlight/highlight-with-init.js" defer="defer"></script></body></html>
\ No newline at end of file
diff --git a/test/src/pub_license/fixtures/noLicense.html b/test/src/pub_license/fixtures/noLicense.html
deleted file mode 100644
index bd81845..0000000
--- a/test/src/pub_license/fixtures/noLicense.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<!DOCTYPE html>
-<html lang="en-us"><head><script src="https://www.googletagmanager.com/gtm.js?id=GTM-MX6DBN9" async="async"></script><script src="/static/hash-i7rnnc9t/js/gtm.js" async="async"></script><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1"/><meta name="robots" content="noindex"/><meta name="twitter:card" content="summary"/><meta name="twitter:site" content="@dart_lang"/><meta name="twitter:description" content="Android通知栏音乐控制"/><meta name="twitter:image" content="https://pub.dev/static/hash-i7rnnc9t/img/pub-dev-icon-cover-image.png"/><meta property="og:type" content="website"/><meta property="og:site_name" content="Dart packages"/><meta property="og:title" content="music_control_notification | Flutter Package"/><meta property="og:description" content="Android通知栏音乐控制"/><meta property="og:image" content="https://pub.dev/static/hash-i7rnnc9t/img/pub-dev-icon-cover-image.png"/><title>music_control_notification | Flutter Package</title><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&amp;family=Google+Sans+Display:wght@400&amp;family=Google+Sans+Text:wght@400;500;700&amp;family=Google+Sans+Mono:wght@400;700&amp;display=swap"/><link rel="shortcut icon" href="/static/hash-i7rnnc9t/img/flutter-logo-32x32.png"/><link rel="stylesheet" href="https://www.gstatic.com/glue/v25_0/ccb.min.css"/><link rel="search" type="application/opensearchdescription+xml" title="Dart packages" href="/osd.xml"/><link rel="canonical" href="https://pub.dev/packages/music_control_notification/license"/><meta name="description" content="Android通知栏音乐控制"/><link rel="alternate" type="application/atom+xml" title="Updated Packages Feed for Pub" href="/feed.atom"/><link rel="stylesheet" type="text/css" href="/static/hash-i7rnnc9t/material/bundle/styles.css"/><link rel="stylesheet" type="text/css" href="/static/hash-i7rnnc9t/css/style.css"/><script src="/static/hash-i7rnnc9t/material/bundle/script.min.js" defer="defer"></script><script src="/static/hash-i7rnnc9t/js/script.dart.js" defer="defer"></script><script src="https://www.gstatic.com/brandstudio/kato/cookie_choice_component/cookie_consent_bar.v3.js" defer="defer" data-autoload-cookie-consent-bar="true"></script><meta name="pub-page-data" content="eyJwa2dEYXRhIjp7InBhY2thZ2UiOiJtdXNpY19jb250cm9sX25vdGlmaWNhdGlvbiIsInZlcnNpb24iOiIwLjAuMSsxIiwibGlrZXMiOjIsImlzRGlzY29udGludWVkIjpmYWxzZX0sInNlc3Npb25Bd2FyZSI6ZmFsc2V9"/><link rel="preload" href="/static/hash-i7rnnc9t/highlight/highlight-with-init.js" as="script"/></head><body class="light-theme"><noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MX6DBN9" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript><div class="site-header"><button class="hamburger" aria-label="menu toggle"></button><a class="logo" href="/"><img class="site-logo" src="/static/hash-i7rnnc9t/img/pub-dev-logo.svg" alt="pub logo" width="140" height="30"/></a><div class="site-header-space"></div><div class="site-header-mask"></div><div class="site-header-search"><form action="/packages" method="GET"><input class="site-header-search-input" name="q" placeholder="New search..." autocomplete="on"/></form></div><div class="site-header-nav scroll-container"><div class="nav-login-container"><button id="-account-login" class="nav-main-button link">Sign in</button></div><div class="nav-container nav-help-container hoverable"><button class="nav-main-button">Help</button><div class="nav-hover-popup"><div class="nav-table-columns"><div class="nav-table-column"><h3>Pub.dev</h3><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div><div class="nav-table-column"><h3>Flutter</h3><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div><div class="nav-table-column"><h3>Dart</h3><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Pub.dev <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Flutter <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Dart <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div id="banner-container"></div><main class="container"><div class="detail-wrapper -active -has-info-box"><div class="detail-header -is-loose"><div class="detail-container"><div class="detail-header-outer-block"><div class="detail-header-content-block"><h1 class="title">music_control_notification 0.0.1+1 <span class="pkg-page-title-copy"><img class="pkg-page-title-copy-icon" src="/static/hash-i7rnnc9t/img/content-copy-icon.svg" alt="icon indicating copy to clipboard operation" width="18" height="18" title="Copy &quot;music_control_notification: ^0.0.1+1&quot; to clipboard" data-copy-content="music_control_notification: ^0.0.1+1" data-ga-click-event="copy-package-version"/><div class="pkg-page-title-copy-feedback"><span class="code">music_control_notification: ^0.0.1+1</span> copied to clipboard</div></span></h1><div class="metadata">Published <span><a class="-x-ago" href="" title="Jul 23, 2019" aria-label="Switch between date and elapsed time." aria-role="button">4 years ago</a></span> <span class="package-badge package-badge-red" title="Package is not compatible with Dart 3.">Dart 3 incompatible</span></div><div class="detail-tags-and-like"><div class="detail-tags"><a class="package-tag unidentified" href="/packages/music_control_notification/score" rel="nofollow" title="Check the Platforms section of the scores tab for details.">[unknown platforms]</a></div><div class="detail-like"><button id="-pub-like-icon-button" class="mdc-icon-button" aria-label="Like this package" data-ga-click-event="toggle-like" aria-pressed="false"><img class="mdc-icon-button__icon" src="/static/hash-i7rnnc9t/img/like-inactive.svg" alt="icon indicating liked status (inactive)" width="18" height="18"/><img class="mdc-icon-button__icon mdc-icon-button__icon--on" src="/static/hash-i7rnnc9t/img/like-active.svg" alt="icon indicating liked status (active)" width="18" height="18"/></button><span class="likes-count"><span id="likes-count">2</span></span></div></div></div></div></div></div><div class="detail-container"><div class="detail-lead"><div class="detail-metadata-toggle"><div class="detail-metadata-toggle-icon">→</div><h3 class="detail-lead-title">Metadata</h3></div><p class="detail-lead-text">Android通知栏音乐控制</p><p class="detail-lead-more"><a class="detail-metadata-toggle">More...</a></p></div></div><div class="detail-body"><div class="detail-tabs"><div class="detail-tabs-wide-header"><div class="detail-container"><ul class="detail-tabs-header"><li class="detail-tab tab-link detail-tab-readme-title" role="button"><a href="/packages/music_control_notification">Readme</a></li><li class="detail-tab tab-link detail-tab-changelog-title" role="button"><a href="/packages/music_control_notification/changelog">Changelog</a></li><li class="detail-tab tab-link detail-tab-example-title" role="button"><a href="/packages/music_control_notification/example">Example</a></li><li class="detail-tab tab-link detail-tab-installing-title" role="button"><a href="/packages/music_control_notification/install">Installing</a></li><li class="detail-tab tab-button detail-tab-license-title -active" role="button">License</li><li class="detail-tab tab-link detail-tab-versions-title" role="button"><a href="/packages/music_control_notification/versions">Versions</a></li><li class="detail-tab tab-link detail-tab-analysis-title" role="button"><a href="/packages/music_control_notification/score">Scores</a></li></ul></div></div><div class="detail-container detail-body-main"><div class="detail-tabs-content"><section class="tab-content detail-tab-license-content -active markdown-body"><h2>License</h2><div class="highlight"><pre>TODO: Add your license here.
-</pre></div></section></div></div></div><aside class="detail-info-box"><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div><a class="packages-scores" href="/packages/music_control_notification/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">2</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">20</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">pub points</div></div><div class="packages-score packages-score-popularity"><div class="packages-score-value -has-value"><span class="packages-score-value-number">0</span><span class="packages-score-value-sign">%</span></div><div class="packages-score-label">popularity</div></div></a><h3 class="title">Publisher</h3><p><span>unverified uploader</span></p><h3 class="title pkg-infobox-metadata">Metadata</h3><p>Android通知栏音乐控制</p><p><a class="link" href="https://github.com/tianyu704/music_control_notification" rel="ugc">Repository (GitHub)</a><br/><a class="link" href="https://github.com/tianyu704/music_control_notification/issues" rel="ugc">View&#47;report issues</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img" src="/static/hash-i7rnnc9t/img/material-icon-balance.svg" alt="Icon for licenses." width="14" height="14"/>unknown (<a href="/packages/music_control_notification/license">LICENSE</a>)</p><h3 class="title">Dependencies</h3><p><a href="https://api.flutter.dev/">flutter</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Amusic_control_notification" rel="nofollow">Packages that depend on music_control_notification</a></p></aside></div><script type="application/ld+json">{"@context":"http\u003a\u002f\u002fschema.org","@type":"SoftwareSourceCode","name":"music\u005fcontrol\u005fnotification","version":"0.0.1\u002b1","description":"music\u005fcontrol\u005fnotification - Android\u901a\u77e5\u680f\u97f3\u4e50\u63a7\u5236","url":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002fmusic\u005fcontrol\u005fnotification","dateCreated":"2019-07-22T09\u003a55\u003a48.802293Z","dateModified":"2019-07-23T08\u003a11\u003a17.693380Z","programmingLanguage":"Dart","image":"https\u003a\u002f\u002fpub.dev\u002fstatic\u002fimg\u002fpub-dev-icon-cover-image.png","license":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002fmusic\u005fcontrol\u005fnotification\u002flicense"}</script></div><div class="detail-metadata"><h3 class="detail-metadata-title"><span class="detail-metadata-toggle">←</span> Metadata</h3><div class="detail-info-box"><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div><a class="packages-scores" href="/packages/music_control_notification/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">2</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">20</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">pub points</div></div><div class="packages-score packages-score-popularity"><div class="packages-score-value -has-value"><span class="packages-score-value-number">0</span><span class="packages-score-value-sign">%</span></div><div class="packages-score-label">popularity</div></div></a><h3 class="title">Publisher</h3><p><span>unverified uploader</span></p><h3 class="title pkg-infobox-metadata">Metadata</h3><p>Android通知栏音乐控制</p><p><a class="link" href="https://github.com/tianyu704/music_control_notification" rel="ugc">Repository (GitHub)</a><br/><a class="link" href="https://github.com/tianyu704/music_control_notification/issues" rel="ugc">View&#47;report issues</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img" src="/static/hash-i7rnnc9t/img/material-icon-balance.svg" alt="Icon for licenses." width="14" height="14"/>unknown (<a href="/packages/music_control_notification/license">LICENSE</a>)</p><h3 class="title">Dependencies</h3><p><a href="https://api.flutter.dev/">flutter</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Amusic_control_notification" rel="nofollow">Packages that depend on music_control_notification</a></p></div><p class="detail-lead-back"><a class="detail-metadata-toggle">Back</a></p></div></main><footer class="site-footer"><a class="link" href="https://dart.dev/">Dart language</a><a class="link sep" href="/policy">Policy</a><a class="link sep" href="https://www.google.com/intl/en/policies/terms/">Terms</a><a class="link sep" href="https://developers.google.com/terms/">API Terms</a><a class="link sep" href="/security">Security</a><a class="link sep" href="https://www.google.com/intl/en/policies/privacy/">Privacy</a><a class="link sep" href="/help">Help</a><a class="link icon sep" href="/feed.atom"><img class="inline-icon" src="/static/hash-i7rnnc9t/img/rss-feed-icon.svg" alt="RSS icon" width="20" height="20" title="RSS/atom feed"/></a><a class="link icon github_issue" href="https://github.com/dart-lang/pub-dev/issues/new"><img class="inline-icon" src="/static/hash-i7rnnc9t/img/bug-report-white-96px.png" alt="bug report icon" width="20" height="20" title="Report an issue with this site"/></a></footer><script src="/static/hash-i7rnnc9t/highlight/highlight-with-init.js" defer="defer"></script></body></html>
\ No newline at end of file
diff --git a/test/src/pub_license/fixtures/singleLicense.html b/test/src/pub_license/fixtures/singleLicense.html
deleted file mode 100644
index f64e3a3..0000000
--- a/test/src/pub_license/fixtures/singleLicense.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<html lang="en-us"><head><script src="https://www.googletagmanager.com/gtm.js?id=GTM-MX6DBN9" async="async"></script><script src="/static/hash-i7rnnc9t/js/gtm.js" async="async"></script><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1"/><meta name="twitter:card" content="summary"/><meta name="twitter:site" content="@dart_lang"/><meta name="twitter:description" content="A Very Good Command-Line Interface for Dart created by Very Good Ventures."/><meta name="twitter:image" content="https://pub.dev/static/hash-i7rnnc9t/img/pub-dev-icon-cover-image.png"/><meta property="og:type" content="website"/><meta property="og:site_name" content="Dart packages"/><meta property="og:title" content="very_good_cli | Dart Package"/><meta property="og:description" content="A Very Good Command-Line Interface for Dart created by Very Good Ventures."/><meta property="og:image" content="https://pub.dev/static/hash-i7rnnc9t/img/pub-dev-icon-cover-image.png"/><meta property="og:url" content="https://pub.dev/packages/very_good_cli/license"/><title>very_good_cli | Dart Package</title><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&amp;family=Google+Sans+Display:wght@400&amp;family=Google+Sans+Text:wght@400;500;700&amp;family=Google+Sans+Mono:wght@400;700&amp;display=swap"/><link rel="shortcut icon" href="/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8"/><link rel="stylesheet" href="https://www.gstatic.com/glue/v25_0/ccb.min.css"/><link rel="search" type="application/opensearchdescription+xml" title="Dart packages" href="/osd.xml"/><link rel="canonical" href="https://pub.dev/packages/very_good_cli/license"/><meta name="description" content="A Very Good Command-Line Interface for Dart created by Very Good Ventures."/><link rel="alternate" type="application/atom+xml" title="Updated Packages Feed for Pub" href="/feed.atom"/><link rel="stylesheet" type="text/css" href="/static/hash-i7rnnc9t/material/bundle/styles.css"/><link rel="stylesheet" type="text/css" href="/static/hash-i7rnnc9t/css/style.css"/><script src="/static/hash-i7rnnc9t/material/bundle/script.min.js" defer="defer"></script><script src="/static/hash-i7rnnc9t/js/script.dart.js" defer="defer"></script><script src="https://www.gstatic.com/brandstudio/kato/cookie_choice_component/cookie_consent_bar.v3.js" defer="defer" data-autoload-cookie-consent-bar="true"></script><meta name="pub-page-data" content="eyJwa2dEYXRhIjp7InBhY2thZ2UiOiJ2ZXJ5X2dvb2RfY2xpIiwidmVyc2lvbiI6IjAuMTYuMCIsImxpa2VzIjo1MTQsInB1Ymxpc2hlcklkIjoidmVyeWdvb2QudmVudHVyZXMiLCJpc0Rpc2NvbnRpbnVlZCI6ZmFsc2V9LCJzZXNzaW9uQXdhcmUiOmZhbHNlfQ=="/><link rel="preload" href="/static/hash-i7rnnc9t/highlight/highlight-with-init.js" as="script"/></head><body class="light-theme"><noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MX6DBN9" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript><div class="site-header"><button class="hamburger" aria-label="menu toggle"></button><a class="logo" href="/"><img class="site-logo" src="/static/hash-i7rnnc9t/img/pub-dev-logo.svg" alt="pub logo" width="140" height="30"/></a><div class="site-header-space"></div><div class="site-header-mask"></div><div class="site-header-search"><form action="/packages" method="GET"><input class="site-header-search-input" name="q" placeholder="New search..." autocomplete="on"/></form></div><div class="site-header-nav scroll-container"><div class="nav-login-container"><button id="-account-login" class="nav-main-button link">Sign in</button></div><div class="nav-container nav-help-container hoverable"><button class="nav-main-button">Help</button><div class="nav-hover-popup"><div class="nav-table-columns"><div class="nav-table-column"><h3>Pub.dev</h3><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div><div class="nav-table-column"><h3>Flutter</h3><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div><div class="nav-table-column"><h3>Dart</h3><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Pub.dev <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Flutter <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Dart <img class="foldable-icon" src="/static/hash-i7rnnc9t/img/nav-mobile-foldable-icon.svg" alt="icon to toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div id="banner-container"></div><main class="container"><div class="detail-wrapper -active -has-info-box"><div class="detail-header -is-loose"><div class="detail-container"><div class="detail-header-outer-block"><div class="detail-header-content-block"><h1 class="title">very_good_cli 0.16.0 <span class="pkg-page-title-copy"><img class="pkg-page-title-copy-icon" src="/static/hash-i7rnnc9t/img/content-copy-icon.svg" alt="icon indicating copy to clipboard operation" width="18" height="18" title="Copy &quot;very_good_cli: ^0.16.0&quot; to clipboard" data-copy-content="very_good_cli: ^0.16.0" data-ga-click-event="copy-package-version"/><div class="pkg-page-title-copy-feedback"><span class="code">very_good_cli: ^0.16.0</span> copied to clipboard</div></span></h1><div class="metadata">Published <span><a class="-x-ago" href="" title="Sep 18, 2023" aria-label="Switch between date and elapsed time." aria-role="button">10 days ago</a></span> • <a class="-pub-publisher" href="/publishers/verygood.ventures"><img class="-pub-publisher-shield" src="/static/hash-i7rnnc9t/img/material-icon-verified.svg" alt="verified publisher icon" width="14" height="14" title="Published by a pub.dev verified publisher"/>verygood.ventures</a><span class="package-badge" title="Package is compatible with Dart 3.">Dart 3 compatible</span></div><div class="detail-tags-and-like"><div class="detail-tags"><div class="-pub-tag-badge"><span class="tag-badge-main">SDK</span><a class="tag-badge-sub" href="/packages?q=sdk%3Adart" rel="nofollow" title="Packages compatible with Dart SDK">Dart</a><a class="tag-badge-sub" href="/packages?q=sdk%3Aflutter" rel="nofollow" title="Packages compatible with Flutter SDK">Flutter</a></div><div class="-pub-tag-badge"><span class="tag-badge-main">Platform</span><a class="tag-badge-sub" href="/packages?q=platform%3Aandroid" rel="nofollow" title="Packages compatible with Android platform">Android</a><a class="tag-badge-sub" href="/packages?q=platform%3Aios" rel="nofollow" title="Packages compatible with iOS platform">iOS</a><a class="tag-badge-sub" href="/packages?q=platform%3Alinux" rel="nofollow" title="Packages compatible with Linux platform">Linux</a><a class="tag-badge-sub" href="/packages?q=platform%3Amacos" rel="nofollow" title="Packages compatible with macOS platform">macOS</a><a class="tag-badge-sub" href="/packages?q=platform%3Aweb" rel="nofollow" title="Packages compatible with Web platform">web</a><a class="tag-badge-sub" href="/packages?q=platform%3Awindows" rel="nofollow" title="Packages compatible with Windows platform">Windows</a></div></div><div class="detail-like"><button id="-pub-like-icon-button" class="mdc-icon-button" aria-label="Like this package" data-ga-click-event="toggle-like" aria-pressed="false"><img class="mdc-icon-button__icon" src="/static/hash-i7rnnc9t/img/like-inactive.svg" alt="icon indicating liked status (inactive)" width="18" height="18"/><img class="mdc-icon-button__icon mdc-icon-button__icon--on" src="/static/hash-i7rnnc9t/img/like-active.svg" alt="icon indicating liked status (active)" width="18" height="18"/></button><span class="likes-count"><span id="likes-count">514</span></span></div></div></div></div></div></div><div class="detail-container"><div class="detail-lead"><div class="detail-metadata-toggle"><div class="detail-metadata-toggle-icon">→</div><h3 class="detail-lead-title">Metadata</h3></div><p class="detail-lead-text">A Very Good Command-Line Interface for Dart created by Very Good Ventures.</p><p class="detail-lead-more"><a class="detail-metadata-toggle">More...</a></p></div></div><div class="detail-body"><div class="detail-tabs"><div class="detail-tabs-wide-header"><div class="detail-container"><ul class="detail-tabs-header"><li class="detail-tab tab-link detail-tab-readme-title" role="button"><a href="/packages/very_good_cli">Readme</a></li><li class="detail-tab tab-link detail-tab-changelog-title" role="button"><a href="/packages/very_good_cli/changelog">Changelog</a></li><li class="detail-tab tab-link detail-tab-example-title" role="button"><a href="/packages/very_good_cli/example">Example</a></li><li class="detail-tab tab-link detail-tab-installing-title" role="button"><a href="/packages/very_good_cli/install">Installing</a></li><li class="detail-tab tab-button detail-tab-license-title -active" role="button">License</li><li class="detail-tab tab-link detail-tab-versions-title" role="button"><a href="/packages/very_good_cli/versions">Versions</a></li><li class="detail-tab tab-link detail-tab-analysis-title" role="button"><a href="/packages/very_good_cli/score">Scores</a></li></ul></div></div><div class="detail-container detail-body-main"><div class="detail-tabs-content"><section class="tab-content detail-tab-license-content -active markdown-body"><h2>License</h2><div class="highlight"><pre>MIT License
-
-Copyright (c) 2021 Very Good Ventures
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the &quot;Software&quot;), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and&#47;or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.</pre></div></section></div></div></div><aside class="detail-info-box"><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div><a class="packages-scores" href="/packages/very_good_cli/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">514</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">140</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">pub points</div></div><div class="packages-score packages-score-popularity"><div class="packages-score-value -has-value"><span class="packages-score-value-number">79</span><span class="packages-score-value-sign">%</span></div><div class="packages-score-label">popularity</div></div></a><h3 class="title">Publisher</h3><p><a href="/publishers/verygood.ventures"><img class="-pub-publisher-shield" src="/static/hash-i7rnnc9t/img/material-icon-verified.svg" alt="verified publisher icon" width="14" height="14" title="Published by a pub.dev verified publisher"/>verygood.ventures</a></p><h3 class="title pkg-infobox-metadata">Metadata</h3><p>A Very Good Command-Line Interface for Dart created by Very Good Ventures.</p><p><a class="link" href="https://cli.vgv.dev/" rel="ugc">Homepage</a><br/><a class="link" href="https://github.com/VeryGoodOpenSource/very_good_cli" rel="ugc">Repository (GitHub)</a><br/><a class="link" href="https://github.com/VeryGoodOpenSource/very_good_cli/issues" rel="ugc">View&#47;report issues</a><br/><a class="link" href="https://github.com/VeryGoodOpenSource/very_good_cli/blob/main/CONTRIBUTING.md" rel="ugc">Contributing</a><br/></p><h3 class="title">Topics</h3><p><a href="/packages?q=topic%3Acli" rel="nofollow">#cli</a> <a href="/packages?q=topic%3Acodegen" rel="nofollow">#codegen</a> <a href="/packages?q=topic%3Atest" rel="nofollow">#test</a> <a href="/packages?q=topic%3Atemplate" rel="nofollow">#template</a> <a href="/packages?q=topic%3Aboilerplate" rel="nofollow">#boilerplate</a></p><h3 class="title">Documentation</h3><p><a class="link" href="https://cli.vgv.dev/docs/overview" rel="ugc">Documentation</a><br/><a class="link" href="/documentation/very_good_cli/latest/">API reference</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img" src="/static/hash-i7rnnc9t/img/material-icon-balance.svg" alt="Icon for licenses." width="14" height="14"/>MIT (<a href="/packages/very_good_cli/license">LICENSE</a>)</p><h3 class="title">Dependencies</h3><p><a href="/packages/args" title="^2.1.0">args</a>, <a href="/packages/cli_completion" title="^0.3.0">cli_completion</a>, <a href="/packages/collection" title="^1.17.1">collection</a>, <a href="/packages/glob" title="^2.0.2">glob</a>, <a href="/packages/lcov_parser" title="^0.1.2">lcov_parser</a>, <a href="/packages/mason" title="0.1.0-dev.50">mason</a>, <a href="/packages/mason_logger" title="^0.2.2">mason_logger</a>, <a href="/packages/meta" title="^1.3.0">meta</a>, <a href="/packages/path" title="^1.8.0">path</a>, <a href="/packages/pub_updater" title="^0.3.1">pub_updater</a>, <a href="/packages/pubspec_parse" title="^1.2.0">pubspec_parse</a>, <a href="/packages/stack_trace" title="^1.10.0">stack_trace</a>, <a href="/packages/universal_io" title="^2.0.4">universal_io</a>, <a href="/packages/very_good_test_runner" title="^0.1.2">very_good_test_runner</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Avery_good_cli" rel="nofollow">Packages that depend on very_good_cli</a></p></aside></div><script type="application/ld+json">{"@context":"http\u003a\u002f\u002fschema.org","@type":"SoftwareSourceCode","name":"very\u005fgood\u005fcli","version":"0.16.0","description":"very\u005fgood\u005fcli - A Very Good Command-Line Interface for Dart created by Very Good Ventures.","url":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002fvery\u005fgood\u005fcli","dateCreated":"2021-02-16T23\u003a08\u003a13.540566Z","dateModified":"2023-09-18T16\u003a20\u003a45.174700Z","programmingLanguage":"Dart","image":"https\u003a\u002f\u002fpub.dev\u002fstatic\u002fimg\u002fpub-dev-icon-cover-image.png","license":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002fvery\u005fgood\u005fcli\u002flicense"}</script></div><div class="detail-metadata"><h3 class="detail-metadata-title"><span class="detail-metadata-toggle">←</span> Metadata</h3><div class="detail-info-box"><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-i7rnnc9t/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div><a class="packages-scores" href="/packages/very_good_cli/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">514</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">140</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">pub points</div></div><div class="packages-score packages-score-popularity"><div class="packages-score-value -has-value"><span class="packages-score-value-number">79</span><span class="packages-score-value-sign">%</span></div><div class="packages-score-label">popularity</div></div></a><h3 class="title">Publisher</h3><p><a href="/publishers/verygood.ventures"><img class="-pub-publisher-shield" src="/static/hash-i7rnnc9t/img/material-icon-verified.svg" alt="verified publisher icon" width="14" height="14" title="Published by a pub.dev verified publisher"/>verygood.ventures</a></p><h3 class="title pkg-infobox-metadata">Metadata</h3><p>A Very Good Command-Line Interface for Dart created by Very Good Ventures.</p><p><a class="link" href="https://cli.vgv.dev/" rel="ugc">Homepage</a><br/><a class="link" href="https://github.com/VeryGoodOpenSource/very_good_cli" rel="ugc">Repository (GitHub)</a><br/><a class="link" href="https://github.com/VeryGoodOpenSource/very_good_cli/issues" rel="ugc">View&#47;report issues</a><br/><a class="link" href="https://github.com/VeryGoodOpenSource/very_good_cli/blob/main/CONTRIBUTING.md" rel="ugc">Contributing</a><br/></p><h3 class="title">Topics</h3><p><a href="/packages?q=topic%3Acli" rel="nofollow">#cli</a> <a href="/packages?q=topic%3Acodegen" rel="nofollow">#codegen</a> <a href="/packages?q=topic%3Atest" rel="nofollow">#test</a> <a href="/packages?q=topic%3Atemplate" rel="nofollow">#template</a> <a href="/packages?q=topic%3Aboilerplate" rel="nofollow">#boilerplate</a></p><h3 class="title">Documentation</h3><p><a class="link" href="https://cli.vgv.dev/docs/overview" rel="ugc">Documentation</a><br/><a class="link" href="/documentation/very_good_cli/latest/">API reference</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img" src="/static/hash-i7rnnc9t/img/material-icon-balance.svg" alt="Icon for licenses." width="14" height="14"/>MIT (<a href="/packages/very_good_cli/license">LICENSE</a>)</p><h3 class="title">Dependencies</h3><p><a href="/packages/args" title="^2.1.0">args</a>, <a href="/packages/cli_completion" title="^0.3.0">cli_completion</a>, <a href="/packages/collection" title="^1.17.1">collection</a>, <a href="/packages/glob" title="^2.0.2">glob</a>, <a href="/packages/lcov_parser" title="^0.1.2">lcov_parser</a>, <a href="/packages/mason" title="0.1.0-dev.50">mason</a>, <a href="/packages/mason_logger" title="^0.2.2">mason_logger</a>, <a href="/packages/meta" title="^1.3.0">meta</a>, <a href="/packages/path" title="^1.8.0">path</a>, <a href="/packages/pub_updater" title="^0.3.1">pub_updater</a>, <a href="/packages/pubspec_parse" title="^1.2.0">pubspec_parse</a>, <a href="/packages/stack_trace" title="^1.10.0">stack_trace</a>, <a href="/packages/universal_io" title="^2.0.4">universal_io</a>, <a href="/packages/very_good_test_runner" title="^0.1.2">very_good_test_runner</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Avery_good_cli" rel="nofollow">Packages that depend on very_good_cli</a></p></div><p class="detail-lead-back"><a class="detail-metadata-toggle">Back</a></p></div></main><footer class="site-footer"><a class="link" href="https://dart.dev/">Dart language</a><a class="link sep" href="/policy">Policy</a><a class="link sep" href="https://www.google.com/intl/en/policies/terms/">Terms</a><a class="link sep" href="https://developers.google.com/terms/">API Terms</a><a class="link sep" href="/security">Security</a><a class="link sep" href="https://www.google.com/intl/en/policies/privacy/">Privacy</a><a class="link sep" href="/help">Help</a><a class="link icon sep" href="/feed.atom"><img class="inline-icon" src="/static/hash-i7rnnc9t/img/rss-feed-icon.svg" alt="RSS icon" width="20" height="20" title="RSS/atom feed"/></a><a class="link icon github_issue" href="https://github.com/dart-lang/pub-dev/issues/new"><img class="inline-icon" src="/static/hash-i7rnnc9t/img/bug-report-white-96px.png" alt="bug report icon" width="20" height="20" title="Report an issue with this site"/></a></footer><script src="/static/hash-i7rnnc9t/highlight/highlight-with-init.js" defer="defer"></script></body></html>
\ No newline at end of file
diff --git a/test/src/pub_license/pub_license_test.dart b/test/src/pub_license/pub_license_test.dart
deleted file mode 100644
index 9b346e1..0000000
--- a/test/src/pub_license/pub_license_test.dart
+++ /dev/null
@@ -1,223 +0,0 @@
-import 'dart:io';
-
-import 'package:html/dom.dart' as html_dom;
-import 'package:html/parser.dart' as html_parser;
-import 'package:http/http.dart' as http;
-import 'package:mocktail/mocktail.dart';
-import 'package:path/path.dart' as path;
-import 'package:test/test.dart';
-import 'package:very_good_cli/src/pub_license/pub_license.dart';
-
-class _MockClient extends Mock implements http.Client {}
-
-class _MockResponse extends Mock implements http.Response {}
-
-class _MockParseError extends Mock implements html_parser.ParseError {
-  @override
-  String toString({dynamic color}) => super.toString();
-}
-
-class _MockDocument extends Mock implements html_dom.Document {}
-
-class _MockElement extends Mock implements html_dom.Element {}
-
-void main() {
-  group('PubLicense', () {
-    late http.Client client;
-    late http.Response response;
-
-    setUpAll(() {
-      registerFallbackValue(Uri.parse('https://vgv.dev/'));
-    });
-
-    setUp(() {
-      response = _MockResponse();
-      when(() => response.statusCode).thenReturn(200);
-      when(() => response.body).thenReturn('');
-
-      client = _MockClient();
-      when(() => client.get(any())).thenAnswer((_) async => response);
-    });
-
-    test('can be instantiated', () {
-      expect(PubLicense(), isA<PubLicense>());
-    });
-
-    group('getLicense', () {
-      group('returns as expected', () {
-        String fixturePath(String name) => path.joinAll([
-              Directory.current.path,
-              'test',
-              'src',
-              'pub_license',
-              'fixtures',
-              '$name.html',
-            ]);
-
-        test('when parsing a single license fixture', () async {
-          final fixture = File(fixturePath('singleLicense')).readAsStringSync();
-          when(() => response.body).thenReturn(fixture);
-
-          final pubLicense = PubLicense(client: client);
-          final license = await pubLicense.getLicense('very_good_cli');
-
-          expect(license.length, equals(1));
-          expect(license.first, equals('MIT'));
-        });
-
-        test('when parsing a multiple license fixture', () async {
-          final fixture =
-              File(fixturePath('multipleLicense')).readAsStringSync();
-          when(() => response.body).thenReturn(fixture);
-
-          final pubLicense = PubLicense(client: client);
-          final license = await pubLicense.getLicense('just_audio');
-
-          expect(license.length, equals(2));
-          expect(license.first, equals('Apache-2.0'));
-          expect(license.last, equals('MIT'));
-        });
-
-        test('when parsing a no license fixture', () async {
-          final fixture = File(fixturePath('noLicense')).readAsStringSync();
-          when(() => response.body).thenReturn(fixture);
-
-          final pubLicense = PubLicense(client: client);
-          final license = await pubLicense.getLicense(
-            'music_control_notification',
-          );
-
-          expect(license.length, equals(1));
-          expect(license.first, equals('unknown'));
-        });
-      });
-
-      group('throws a PubLicenseException', () {
-        test('when statusCode is not 200', () async {
-          when(() => response.statusCode).thenReturn(404);
-
-          final pubLicense = PubLicense(client: client);
-
-          final errorMessage =
-              '''[pub_license] Failed to retrieve the license of the package, received status code: ${response.statusCode}''';
-          await expectLater(
-            () => pubLicense.getLicense('very_good_cli'),
-            throwsA(
-              isA<PubLicenseException>().having(
-                (exception) => exception.message,
-                'message',
-                equals(errorMessage),
-              ),
-            ),
-          );
-        });
-
-        group('when parsing fails', () {
-          test('with a ParseError', () async {
-            final parseError = _MockParseError();
-            final pubLicense = PubLicense(
-              client: client,
-              parse: (input, {encoding, generateSpans = true, sourceUrl}) =>
-                  throw parseError,
-            );
-
-            final errorMessage =
-                '''[pub_license] Failed to parse the response body, received error: $parseError''';
-            await expectLater(
-              () => pubLicense.getLicense('very_good_cli'),
-              throwsA(
-                isA<PubLicenseException>().having(
-                  (exception) => exception.message,
-                  'message',
-                  equals(errorMessage),
-                ),
-              ),
-            );
-          });
-
-          test('with an unexpected error', () async {
-            const error = 'unexpected error';
-            final pubLicense = PubLicense(
-              client: client,
-              parse: (input, {encoding, generateSpans = true, sourceUrl}) =>
-                  // ignore: only_throw_errors
-                  throw error,
-            );
-
-            const errorMessage =
-                '''[pub_license] An unknown error occurred when trying to parse the response body, received error: $error''';
-            await expectLater(
-              () => pubLicense.getLicense('very_good_cli'),
-              throwsA(
-                isA<PubLicenseException>().having(
-                  (exception) => exception.message,
-                  'message',
-                  equals(errorMessage),
-                ),
-              ),
-            );
-          });
-        });
-
-        group('when scraping fails', () {
-          late html_dom.Document document;
-          late html_dom.Element element;
-
-          setUp(() {
-            document = _MockDocument();
-            element = _MockElement();
-          });
-
-          test('due to missing `.detail-info-box`', () async {
-            when(() => document.querySelector('.detail-info-box'))
-                .thenReturn(null);
-
-            final pubLicense = PubLicense(
-              client: client,
-              parse: (input, {encoding, generateSpans = true, sourceUrl}) =>
-                  document,
-            );
-
-            const errorMessage =
-                '''[pub_license] Failed to scrape license because `.detail-info-box` was not found.''';
-            await expectLater(
-              () => pubLicense.getLicense('very_good_cli'),
-              throwsA(
-                isA<PubLicenseException>().having(
-                  (exception) => exception.message,
-                  'message',
-                  equals(errorMessage),
-                ),
-              ),
-            );
-          });
-
-          test('due to missing license header', () async {
-            when(() => document.querySelector('.detail-info-box'))
-                .thenReturn(element);
-            when(() => element.children).thenReturn([]);
-
-            final pubLicense = PubLicense(
-              client: client,
-              parse: (input, {encoding, generateSpans = true, sourceUrl}) =>
-                  document,
-            );
-
-            const errorMessage =
-                '''[pub_license] Failed to scrape license because the license header was not found.''';
-            await expectLater(
-              () => pubLicense.getLicense('very_good_cli'),
-              throwsA(
-                isA<PubLicenseException>().having(
-                  (exception) => exception.message,
-                  'message',
-                  equals(errorMessage),
-                ),
-              ),
-            );
-          });
-        });
-      });
-    });
-  });
-}