ff03f74404
Implements `flynn gmail-auth` to complete the OAuth2 flow that GmailWatcher references but was never built. Supports local callback server (default) and --manual paste mode. Adds Gmail health check to `flynn doctor`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import { readCredentials, generateAuthUrl, saveToken } from './gmail-auth.js';
|
|
import { writeFileSync, mkdirSync, rmSync, readFileSync, statSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
describe('gmail-auth', () => {
|
|
const testDir = join(tmpdir(), 'flynn-test-gmail-auth');
|
|
|
|
afterEach(() => {
|
|
try { rmSync(testDir, { recursive: true }); } catch {}
|
|
});
|
|
|
|
describe('readCredentials', () => {
|
|
it('reads installed credentials', () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const credPath = join(testDir, 'creds.json');
|
|
writeFileSync(credPath, JSON.stringify({
|
|
installed: {
|
|
client_id: 'test-id',
|
|
client_secret: 'test-secret',
|
|
redirect_uris: ['http://localhost:3000'],
|
|
},
|
|
}));
|
|
|
|
const creds = readCredentials(credPath);
|
|
expect(creds.client_id).toBe('test-id');
|
|
expect(creds.client_secret).toBe('test-secret');
|
|
expect(creds.redirect_uris).toEqual(['http://localhost:3000']);
|
|
});
|
|
|
|
it('reads web credentials', () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const credPath = join(testDir, 'creds.json');
|
|
writeFileSync(credPath, JSON.stringify({
|
|
web: {
|
|
client_id: 'web-id',
|
|
client_secret: 'web-secret',
|
|
},
|
|
}));
|
|
|
|
const creds = readCredentials(credPath);
|
|
expect(creds.client_id).toBe('web-id');
|
|
expect(creds.client_secret).toBe('web-secret');
|
|
});
|
|
|
|
it('throws when file does not exist', () => {
|
|
expect(() => readCredentials('/nonexistent/creds.json')).toThrow('not found');
|
|
});
|
|
|
|
it('throws when client_id is missing', () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const credPath = join(testDir, 'creds.json');
|
|
writeFileSync(credPath, JSON.stringify({ installed: { client_secret: 's' } }));
|
|
|
|
expect(() => readCredentials(credPath)).toThrow('missing client_id or client_secret');
|
|
});
|
|
|
|
it('throws when client_secret is missing', () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const credPath = join(testDir, 'creds.json');
|
|
writeFileSync(credPath, JSON.stringify({ installed: { client_id: 'id' } }));
|
|
|
|
expect(() => readCredentials(credPath)).toThrow('missing client_id or client_secret');
|
|
});
|
|
});
|
|
|
|
describe('generateAuthUrl', () => {
|
|
it('generates correct OAuth2 URL', () => {
|
|
const url = generateAuthUrl('my-client-id', 'my-secret', 'http://localhost:3000');
|
|
expect(url).toContain('https://accounts.google.com/o/oauth2/v2/auth');
|
|
expect(url).toContain('client_id=my-client-id');
|
|
expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%3A3000');
|
|
expect(url).toContain('scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly');
|
|
expect(url).toContain('access_type=offline');
|
|
expect(url).toContain('prompt=consent');
|
|
});
|
|
});
|
|
|
|
describe('saveToken', () => {
|
|
it('saves token as JSON with 0o600 permissions', () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const tokenPath = join(testDir, 'token.json');
|
|
const token = { access_token: 'abc', refresh_token: 'def' };
|
|
|
|
saveToken(tokenPath, token);
|
|
|
|
const saved = JSON.parse(readFileSync(tokenPath, 'utf-8'));
|
|
expect(saved).toEqual(token);
|
|
|
|
const stats = statSync(tokenPath);
|
|
// Check owner-only read/write (0o600)
|
|
expect(stats.mode & 0o777).toBe(0o600);
|
|
});
|
|
|
|
it('creates parent directories if needed', () => {
|
|
const tokenPath = join(testDir, 'nested', 'dir', 'token.json');
|
|
saveToken(tokenPath, { token: 'value' });
|
|
|
|
const saved = JSON.parse(readFileSync(tokenPath, 'utf-8'));
|
|
expect(saved).toEqual({ token: 'value' });
|
|
});
|
|
});
|
|
});
|