Skip to content

Commit e6dce85

Browse files
Improve test suite auth handling and CI workflow
Refactor auth validation into reusable module with getGitHubAuth(), getSessionSkipReason(), and getPatSkipReason() exports. Add global setup to warn about missing/invalid auth. Segment CI tests by auth type (@auth-pat, @auth-session) with explicit validation before running. - validate-github-authorization.js: Export functions for programmatic use - obtain-github-authorization.js: Support stdout redirection (>> .env) - playwright.config.js: Load .env via dotenv, add global setup - tests/global-setup.js: Validate auth and show skip warnings at test start - extension.spec.js: Use @auth-pat/@auth-session/@pat-config tags, skip on invalid auth - test.yml: Validate PAT, run tests in auth-aware groups - package.json: Add dotenv dependency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cf1728d commit e6dce85

8 files changed

Lines changed: 228 additions & 68 deletions

File tree

.github/workflows/test.yml

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ jobs:
1919
os: [macos-latest, ubuntu-latest, windows-latest]
2020
runs-on: ${{ matrix.os }}
2121
timeout-minutes: 15
22-
# TODO: Remove continue-on-error once #6 (flaky Windows test) is resolved
23-
continue-on-error: ${{ matrix.os == 'windows-latest' }}
2422

2523
steps:
2624
- name: Checkout
@@ -59,10 +57,43 @@ jobs:
5957
- name: Build Chrome extension
6058
run: npm run build:chrome
6159

62-
- name: Run tests
63-
run: npm test
60+
- name: Validate GitHub API PAT
61+
id: validate-pat
62+
shell: bash
6463
env:
6564
API_CONTRACT_TEST_PAT: ${{ secrets.API_CONTRACT_TEST_PAT }}
65+
run: |
66+
if [ -z "$API_CONTRACT_TEST_PAT" ]; then
67+
echo "error_msg=API_CONTRACT_TEST_PAT secret is not configured. The API contract test requires this secret." >> "$GITHUB_OUTPUT"
68+
exit 0
69+
fi
70+
response=$(curl -s -o /dev/null -w "%{http_code}" \
71+
-H "Authorization: Bearer $API_CONTRACT_TEST_PAT" \
72+
https://api.github.com/rate_limit)
73+
if [ "$response" = "401" ]; then
74+
echo "error_msg=API_CONTRACT_TEST_PAT is expired or invalid (HTTP 401). Please update the repository secret." >> "$GITHUB_OUTPUT"
75+
elif [ "$response" != "200" ]; then
76+
echo "error_msg=GitHub API returned HTTP $response when validating PAT." >> "$GITHUB_OUTPUT"
77+
fi
78+
79+
- name: Run tests (excluding @auth-pat and @auth-session)
80+
run: npm test -- --grep-invert "@auth-pat|@auth-session"
81+
82+
- name: Run @auth-pat tests
83+
if: ${{ !steps.validate-pat.outputs.error_msg }}
84+
run: npm test -- --grep @auth-pat
85+
env:
86+
API_CONTRACT_TEST_PAT: ${{ secrets.API_CONTRACT_TEST_PAT }}
87+
88+
- name: Fail @auth-pat tests (PAT issue)
89+
if: ${{ steps.validate-pat.outputs.error_msg }}
90+
shell: bash
91+
run: |
92+
echo "::error::${{ steps.validate-pat.outputs.error_msg }}"
93+
exit 1
94+
95+
- name: Skip @auth-session tests (no session auth in CI)
96+
run: echo "::notice::@auth-session tests skipped - GitHub session auth not available in CI"
6697

6798
- name: Upload Chrome extension
6899
uses: actions/upload-artifact@v4

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"devDependencies": {
2626
"@playwright/test": "^1.57.0",
2727
"chokidar": "^5.0.0",
28+
"dotenv": "^17.2.3",
2829
"web-ext": "^9.0.0"
2930
}
3031
}

playwright.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { defineConfig, devices } from '@playwright/test';
2+
import dotenv from 'dotenv';
3+
4+
// Load .env file for local development (GITHUB_AUTH_STATE, API_CONTRACT_TEST_PAT)
5+
dotenv.config({ quiet: true });
26

37
// Prevent list reporter from truncating test names
48
process.stdout.columns = 200;
59

610
export default defineConfig({
11+
globalSetup: './tests/global-setup.js',
712
testDir: './tests',
813
snapshotPathTemplate: '{snapshotDir}/{arg}{ext}',
914
snapshotDir: './tests/screenshots',

scripts/obtain-github-authorization.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { chromium } from '@playwright/test';
44
import * as readline from 'readline';
55

66
async function main() {
7-
console.log('Opening browser for GitHub login...');
8-
console.log('Log in, then press Enter here when done.\n');
7+
console.error('Opening browser for GitHub login...');
8+
console.error('Log in, then press Enter here when done.\n');
99

1010
const browser = await chromium.launch({ headless: false });
1111
const context = await browser.newContext();
1212
const page = await context.newPage();
1313
await page.goto('https://github.com/login');
1414

15-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
1616
await new Promise(resolve => rl.question('Press Enter after logging in...', resolve));
1717
rl.close();
1818

@@ -21,8 +21,17 @@ async function main() {
2121

2222
await browser.close();
2323

24-
console.log('\nAdd this to your .env file:\n');
25-
console.log(`GITHUB_AUTH_STATE=${base64}`);
24+
const envLine = `GITHUB_AUTH_STATE=${base64}`;
25+
26+
if (process.stdout.isTTY) {
27+
// Interactive: show instructions and env var
28+
console.log('\nAdd this to your .env file:\n');
29+
console.log(envLine);
30+
} else {
31+
// Redirected: output only env var to stdout, success message to stderr
32+
console.log(envLine);
33+
console.error('\nGITHUB_AUTH_STATE written to stdout');
34+
}
2635
}
2736

2837
main().catch(console.error);
Lines changed: 111 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,126 @@
11
#!/usr/bin/env node
22

3-
const encoded = process.env.GITHUB_AUTH_STATE;
3+
/**
4+
* Validates GitHub session auth from GITHUB_AUTH_STATE environment variable.
5+
* Can be run as CLI or imported as a module.
6+
*/
47

5-
if (!encoded) {
6-
console.error('GITHUB_AUTH_STATE environment variable is not set');
7-
process.exit(1);
8+
/**
9+
* Decodes and validates GITHUB_AUTH_STATE, returns the storage state or null.
10+
* @param {string} [encoded] - Base64-encoded auth state (defaults to env var)
11+
* @returns {object|null} - Playwright storageState object, or null if invalid
12+
*/
13+
export function getGitHubAuth(encoded = process.env.GITHUB_AUTH_STATE) {
14+
if (!encoded) return null;
15+
try {
16+
const decoded = JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
17+
const githubCookies = decoded.cookies?.filter(c => c.domain?.includes('github')) || [];
18+
if (githubCookies.length === 0) return null;
19+
20+
// Check for expired cookies (expires > 0 means it has expiry, not a session cookie)
21+
const expiredCount = githubCookies.filter(c => c.expires > 0 && c.expires * 1000 < Date.now()).length;
22+
if (expiredCount > 0) return null;
23+
24+
return decoded;
25+
} catch {
26+
return null;
27+
}
828
}
929

10-
try {
11-
const decoded = JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
30+
/**
31+
* Returns a descriptive reason why session auth is unavailable, or null if valid.
32+
* @param {string} [encoded] - Base64-encoded auth state (defaults to env var)
33+
* @returns {string|null} - Error message, or null if auth is valid
34+
*/
35+
export function getSessionSkipReason(encoded = process.env.GITHUB_AUTH_STATE) {
36+
if (!encoded) {
37+
return 'GITHUB_AUTH_STATE not set. Run: node scripts/obtain-github-authorization.js';
38+
}
39+
try {
40+
const decoded = JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
41+
const githubCookies = decoded.cookies?.filter(c => c.domain?.includes('github')) || [];
42+
if (githubCookies.length === 0) {
43+
return 'GITHUB_AUTH_STATE has no GitHub cookies';
44+
}
45+
const expiredCount = githubCookies.filter(c => c.expires > 0 && c.expires * 1000 < Date.now()).length;
46+
if (expiredCount > 0) {
47+
return `GITHUB_AUTH_STATE has ${expiredCount} expired cookie(s). Run: node scripts/obtain-github-authorization.js`;
48+
}
49+
return null; // Auth is valid
50+
} catch (e) {
51+
return `GITHUB_AUTH_STATE decode failed: ${e.message}`;
52+
}
53+
}
1254

13-
console.log('Cookies:', decoded.cookies?.length || 0);
14-
console.log('Origins:', decoded.origins?.length || 0);
15-
console.log();
55+
/**
56+
* Validates GitHub PAT by making an API call. Returns skip reason or null if valid.
57+
* @param {string} [pat] - GitHub PAT (defaults to env var)
58+
* @returns {Promise<string|null>} - Error message, or null if PAT is valid
59+
*/
60+
export async function getPatSkipReason(pat = process.env.API_CONTRACT_TEST_PAT) {
61+
if (!pat) {
62+
return 'API_CONTRACT_TEST_PAT not set';
63+
}
64+
try {
65+
const response = await fetch('https://api.github.com/rate_limit', {
66+
headers: { Authorization: `Bearer ${pat}` },
67+
});
68+
if (response.status === 401) {
69+
return 'API_CONTRACT_TEST_PAT is expired or invalid (HTTP 401)';
70+
}
71+
if (!response.ok) {
72+
return `API_CONTRACT_TEST_PAT validation failed (HTTP ${response.status})`;
73+
}
74+
return null; // PAT is valid
75+
} catch (e) {
76+
return `API_CONTRACT_TEST_PAT validation error: ${e.message}`;
77+
}
78+
}
1679

17-
const githubCookies = decoded.cookies?.filter(c => c.domain?.includes('github')) || [];
80+
// CLI behavior when run directly
81+
const isMain = process.argv[1]?.endsWith('validate-github-authorization.js');
82+
if (isMain) {
83+
const encoded = process.env.GITHUB_AUTH_STATE;
1884

19-
if (githubCookies.length === 0) {
20-
console.error('No GitHub cookies found - auth state may be invalid');
85+
if (!encoded) {
86+
console.error('GITHUB_AUTH_STATE environment variable is not set');
2187
process.exit(1);
2288
}
2389

24-
console.log('GitHub cookies:');
25-
githubCookies.forEach(c => {
26-
// expires <= 0 means session cookie, not expired
27-
const isSession = !c.expires || c.expires <= 0;
28-
const expires = isSession ? 'session' : new Date(c.expires * 1000).toISOString();
29-
const expired = !isSession && c.expires * 1000 < Date.now() ? ' (EXPIRED)' : '';
30-
console.log(` ${c.name} ${c.domain} ${expires}${expired}`);
31-
});
32-
33-
const expiredCount = githubCookies.filter(c => c.expires > 0 && c.expires * 1000 < Date.now()).length;
34-
if (expiredCount > 0) {
90+
try {
91+
const decoded = JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
92+
93+
console.log('Cookies:', decoded.cookies?.length || 0);
94+
console.log('Origins:', decoded.origins?.length || 0);
95+
console.log();
96+
97+
const githubCookies = decoded.cookies?.filter(c => c.domain?.includes('github')) || [];
98+
99+
if (githubCookies.length === 0) {
100+
console.error('No GitHub cookies found - auth state may be invalid');
101+
process.exit(1);
102+
}
103+
104+
console.log('GitHub cookies:');
105+
githubCookies.forEach(c => {
106+
// expires <= 0 means session cookie, not expired
107+
const isSession = !c.expires || c.expires <= 0;
108+
const expires = isSession ? 'session' : new Date(c.expires * 1000).toISOString();
109+
const expired = !isSession && c.expires * 1000 < Date.now() ? ' (EXPIRED)' : '';
110+
console.log(` ${c.name} ${c.domain} ${expires}${expired}`);
111+
});
112+
113+
const expiredCount = githubCookies.filter(c => c.expires > 0 && c.expires * 1000 < Date.now()).length;
114+
if (expiredCount > 0) {
115+
console.log();
116+
console.error(`${expiredCount} cookie(s) expired - regenerate with: node scripts/obtain-github-authorization.js`);
117+
process.exit(1);
118+
}
119+
35120
console.log();
36-
console.error(`${expiredCount} cookie(s) expired - regenerate with: node scripts/obtain-github-authorization.js`);
121+
console.log('Auth state appears valid');
122+
} catch (e) {
123+
console.error('Failed to decode GITHUB_AUTH_STATE:', e.message);
37124
process.exit(1);
38125
}
39-
40-
console.log();
41-
console.log('Auth state appears valid');
42-
} catch (e) {
43-
console.error('Failed to decode GITHUB_AUTH_STATE:', e.message);
44-
process.exit(1);
45126
}

0 commit comments

Comments
 (0)