Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
57 changes: 57 additions & 0 deletions test/live/smoke_test.dart
Original file line number Diff line number Diff line change
@@ -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);
}
37 changes: 37 additions & 0 deletions test/unit/browser_auth_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
55 changes: 55 additions & 0 deletions tool/coverage_check.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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.');
}
Loading