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' }); }); }); });