Skip to content

Commit 1d4676b

Browse files
test: add browser authentication tests, safe live smoke suite, and coverage ratchet
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9cdd343 commit 1d4676b

4 files changed

Lines changed: 169 additions & 4 deletions

File tree

test/README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1-
# Integration Tests
1+
# Integration and Live Tests
22

3-
The integration tests will run against the live GitHub API. These tests will
4-
verify that the library is properly coded against the actual behavior of the
5-
GitHub API.
3+
## Unit and Contract Tests
64

5+
All unit tests, contract tests, security tests, and model tests run offline without external dependencies:
6+
7+
```bash
8+
dart test
9+
```
10+
11+
## Safe Live Smoke Tests
12+
13+
Safe read-only smoke tests are located in `test/live/smoke_test.dart`.
14+
These tests only perform safe, non-destructive read operations against public GitHub endpoints:
15+
16+
```bash
17+
GITHUB_LIVE_SMOKE=1 dart test test/live/smoke_test.dart
18+
```
19+
20+
## Destructive Integration Tests
21+
22+
The legacy integration tests run against the live GitHub API with write operations.
723
To run these tests a GitHub repository and OAuth token will need to be defined
824
in the `config/config.dart` file.
925

test/live/smoke_test.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import 'dart:io';
2+
3+
import 'package:github/github.dart';
4+
import 'package:test/test.dart';
5+
6+
/// Opt-in safe live smoke test suite.
7+
///
8+
/// Runs only when GITHUB_TOKEN or GITHUB_LIVE_SMOKE=1 is set in the environment.
9+
/// Executes only safe, non-destructive read operations against public GitHub endpoints.
10+
void main() {
11+
final hasToken = Platform.environment.containsKey('GITHUB_TOKEN') ||
12+
Platform.environment['GITHUB_LIVE_SMOKE'] == '1';
13+
14+
group('Live GitHub API Smoke Tests (Read-Only)', () {
15+
late GitHub github;
16+
17+
setUp(() {
18+
final token = Platform.environment['GITHUB_TOKEN'];
19+
github = GitHub(
20+
auth: token != null
21+
? Authentication.withToken(token)
22+
: const Authentication.anonymous(),
23+
);
24+
});
25+
26+
tearDown(() {
27+
github.dispose();
28+
});
29+
30+
test('fetches public repository metadata', () async {
31+
final repo = await github.repositories
32+
.getRepository(RepositorySlug('octocat', 'Hello-World'));
33+
34+
expect(repo.name, equals('Hello-World'));
35+
expect(repo.owner?.login, equals('octocat'));
36+
});
37+
38+
test('fetches API status from status endpoint', () async {
39+
final status = await github.misc.getApiStatus();
40+
expect(status.status, isNotNull);
41+
expect(status.status?.indicator, isNotNull);
42+
});
43+
44+
test('fetches zen message', () async {
45+
final zen = await github.misc.getZen();
46+
expect(zen, isNotEmpty);
47+
});
48+
49+
test('fetches gitignore templates list', () async {
50+
final templates = await github.misc.listGitignoreTemplates();
51+
expect(templates, contains('Dart'));
52+
});
53+
},
54+
skip: !hasToken
55+
? 'Skipped: Set GITHUB_TOKEN or GITHUB_LIVE_SMOKE=1 to run safe live tests.'
56+
: null);
57+
}

test/unit/browser_auth_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@TestOn('chrome')
2+
library;
3+
4+
// ignore: deprecated_member_use
5+
import 'dart:html';
6+
7+
import 'package:github/src/browser/xplat_browser.dart';
8+
import 'package:test/test.dart';
9+
10+
void main() {
11+
group('Browser Authentication', () {
12+
tearDown(() {
13+
window.sessionStorage.clear();
14+
});
15+
16+
test('loads token from sessionStorage by default', () {
17+
window.sessionStorage['GITHUB_TOKEN'] = 'session-pat-12345';
18+
final auth = findAuthenticationFromEnvironment();
19+
expect(auth.isAnonymous, isFalse);
20+
expect(auth.token, equals('session-pat-12345'));
21+
});
22+
23+
test('ignores query string by default even if present in URL', () {
24+
// By default allowQueryAuth is false
25+
final auth = findAuthenticationFromEnvironment();
26+
// Unless present in sessionStorage, it should default to anonymous
27+
expect(auth.isAnonymous, isTrue);
28+
});
29+
30+
test('returns anonymous auth when neither sessionStorage nor query present',
31+
() {
32+
window.sessionStorage.clear();
33+
final auth = findAuthenticationFromEnvironment();
34+
expect(auth.isAnonymous, isTrue);
35+
});
36+
});
37+
}

tool/coverage_check.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'dart:io';
2+
3+
/// Validates that test coverage meets or exceeds the required floor.
4+
///
5+
/// Usage: dart run tool/coverage_check.dart [coverage_file_or_dir] [--min=percentage]
6+
void main(List<String> args) {
7+
var minCoverage = 50.0;
8+
String? targetPath;
9+
10+
for (final arg in args) {
11+
if (arg.startsWith('--min=')) {
12+
minCoverage = double.parse(arg.substring(6));
13+
} else if (!arg.startsWith('--')) {
14+
targetPath = arg;
15+
}
16+
}
17+
18+
targetPath ??= 'coverage/lcov.info';
19+
final file = File(targetPath);
20+
21+
if (!file.existsSync()) {
22+
print('Coverage file not found at $targetPath. Skipping coverage check.');
23+
exit(0);
24+
}
25+
26+
final lines = file.readAsLinesSync();
27+
var linesFound = 0;
28+
var linesHit = 0;
29+
30+
for (final line in lines) {
31+
if (line.startsWith('LF:')) {
32+
linesFound += int.tryParse(line.substring(3).trim()) ?? 0;
33+
} else if (line.startsWith('LH:')) {
34+
linesHit += int.tryParse(line.substring(3).trim()) ?? 0;
35+
}
36+
}
37+
38+
if (linesFound == 0) {
39+
print('No executable lines found in $targetPath.');
40+
exit(0);
41+
}
42+
43+
final percentage = (linesHit / linesFound) * 100.0;
44+
print(
45+
'Test line coverage: ${percentage.toStringAsFixed(2)}% ($linesHit / $linesFound lines hit)');
46+
print('Required coverage floor: ${minCoverage.toStringAsFixed(2)}%');
47+
48+
if (percentage < minCoverage) {
49+
print(
50+
'Error: Coverage ${percentage.toStringAsFixed(2)}% is below required floor of ${minCoverage.toStringAsFixed(2)}%!');
51+
exit(1);
52+
}
53+
54+
print('Coverage check passed successfully.');
55+
}

0 commit comments

Comments
 (0)