diff --git a/test/README.md b/test/README.md index c05bf247..00eb1f60 100644 --- a/test/README.md +++ b/test/README.md @@ -1,9 +1,25 @@ -# Integration Tests +# Integration and Live Tests -The integration tests will run against the live GitHub API. These tests will -verify that the library is properly coded against the actual behavior of the -GitHub API. +## Unit and Contract Tests +All unit tests, contract tests, security tests, and model tests run offline without external dependencies: + +```bash +dart test +``` + +## Safe Live Smoke Tests + +Safe read-only smoke tests are located in `test/live/smoke_test.dart`. +These tests only perform safe, non-destructive read operations against public GitHub endpoints: + +```bash +GITHUB_LIVE_SMOKE=1 dart test test/live/smoke_test.dart +``` + +## Destructive Integration Tests + +The legacy integration tests run against the live GitHub API with write operations. To run these tests a GitHub repository and OAuth token will need to be defined in the `config/config.dart` file. diff --git a/test/live/smoke_test.dart b/test/live/smoke_test.dart new file mode 100644 index 00000000..8fe46813 --- /dev/null +++ b/test/live/smoke_test.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +import 'package:github/github.dart'; +import 'package:test/test.dart'; + +/// Opt-in safe live smoke test suite. +/// +/// Runs only when GITHUB_TOKEN or GITHUB_LIVE_SMOKE=1 is set in the environment. +/// Executes only safe, non-destructive read operations against public GitHub endpoints. +void main() { + final hasToken = Platform.environment.containsKey('GITHUB_TOKEN') || + Platform.environment['GITHUB_LIVE_SMOKE'] == '1'; + + group('Live GitHub API Smoke Tests (Read-Only)', () { + late GitHub github; + + setUp(() { + final token = Platform.environment['GITHUB_TOKEN']; + github = GitHub( + auth: token != null + ? Authentication.withToken(token) + : const Authentication.anonymous(), + ); + }); + + tearDown(() { + github.dispose(); + }); + + test('fetches public repository metadata', () async { + final repo = await github.repositories + .getRepository(RepositorySlug('octocat', 'Hello-World')); + + expect(repo.name, equals('Hello-World')); + expect(repo.owner?.login, equals('octocat')); + }); + + test('fetches API status from status endpoint', () async { + final status = await github.misc.getApiStatus(); + expect(status.status, isNotNull); + expect(status.status?.indicator, isNotNull); + }); + + test('fetches zen message', () async { + final zen = await github.misc.getZen(); + expect(zen, isNotEmpty); + }); + + test('fetches gitignore templates list', () async { + final templates = await github.misc.listGitignoreTemplates(); + expect(templates, contains('Dart')); + }); + }, + skip: !hasToken + ? 'Skipped: Set GITHUB_TOKEN or GITHUB_LIVE_SMOKE=1 to run safe live tests.' + : null); +} diff --git a/test/unit/browser_auth_test.dart b/test/unit/browser_auth_test.dart new file mode 100644 index 00000000..3f45ca61 --- /dev/null +++ b/test/unit/browser_auth_test.dart @@ -0,0 +1,37 @@ +@TestOn('chrome') +library; + +// ignore: deprecated_member_use +import 'dart:html'; + +import 'package:github/src/browser/xplat_browser.dart'; +import 'package:test/test.dart'; + +void main() { + group('Browser Authentication', () { + tearDown(() { + window.sessionStorage.clear(); + }); + + test('loads token from sessionStorage by default', () { + window.sessionStorage['GITHUB_TOKEN'] = 'session-pat-12345'; + final auth = findAuthenticationFromEnvironment(); + expect(auth.isAnonymous, isFalse); + expect(auth.token, equals('session-pat-12345')); + }); + + test('ignores query string by default even if present in URL', () { + // By default allowQueryAuth is false + final auth = findAuthenticationFromEnvironment(); + // Unless present in sessionStorage, it should default to anonymous + expect(auth.isAnonymous, isTrue); + }); + + test('returns anonymous auth when neither sessionStorage nor query present', + () { + window.sessionStorage.clear(); + final auth = findAuthenticationFromEnvironment(); + expect(auth.isAnonymous, isTrue); + }); + }); +} diff --git a/tool/coverage_check.dart b/tool/coverage_check.dart new file mode 100644 index 00000000..bbff09d9 --- /dev/null +++ b/tool/coverage_check.dart @@ -0,0 +1,55 @@ +import 'dart:io'; + +/// Validates that test coverage meets or exceeds the required floor. +/// +/// Usage: dart run tool/coverage_check.dart [coverage_file_or_dir] [--min=percentage] +void main(List args) { + var minCoverage = 50.0; + String? targetPath; + + for (final arg in args) { + if (arg.startsWith('--min=')) { + minCoverage = double.parse(arg.substring(6)); + } else if (!arg.startsWith('--')) { + targetPath = arg; + } + } + + targetPath ??= 'coverage/lcov.info'; + final file = File(targetPath); + + if (!file.existsSync()) { + print('Coverage file not found at $targetPath. Skipping coverage check.'); + exit(0); + } + + final lines = file.readAsLinesSync(); + var linesFound = 0; + var linesHit = 0; + + for (final line in lines) { + if (line.startsWith('LF:')) { + linesFound += int.tryParse(line.substring(3).trim()) ?? 0; + } else if (line.startsWith('LH:')) { + linesHit += int.tryParse(line.substring(3).trim()) ?? 0; + } + } + + if (linesFound == 0) { + print('No executable lines found in $targetPath.'); + exit(0); + } + + final percentage = (linesHit / linesFound) * 100.0; + print( + 'Test line coverage: ${percentage.toStringAsFixed(2)}% ($linesHit / $linesFound lines hit)'); + print('Required coverage floor: ${minCoverage.toStringAsFixed(2)}%'); + + if (percentage < minCoverage) { + print( + 'Error: Coverage ${percentage.toStringAsFixed(2)}% is below required floor of ${minCoverage.toStringAsFixed(2)}%!'); + exit(1); + } + + print('Coverage check passed successfully.'); +}