/** * Unit tests for Cursor authentication module */ import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import type { CursorCredentials } from '../../../src/cursor/types'; import { validateToken, extractUserInfo, saveCredentials, loadCredentials, checkAuthStatus, deleteCredentials, autoDetectTokens, } from '../../../src/cursor/cursor-auth'; // Test isolation let originalCcsHome: string | undefined; let tempDir: string; beforeEach(() => { // Save original CCS_HOME originalCcsHome = process.env.CCS_HOME; // Create temp directory for test isolation tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cursor-test-')); process.env.CCS_HOME = tempDir; }); afterEach(() => { // Restore original CCS_HOME if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } // Clean up temp directory if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } }); describe('validateToken', () => { it('should accept valid token and machineId', () => { const token = 'a'.repeat(50); // 50 chars minimum const machineId = 'a'.repeat(32); // 32 hex chars expect(validateToken(token, machineId)).toBe(true); }); it('should reject 49-char token (too short)', () => { const token = 'a'.repeat(49); // Just under minimum const machineId = 'a'.repeat(32); expect(validateToken(token, machineId)).toBe(false); }); it('should reject 31-char hex UUID (too short)', () => { const token = 'a'.repeat(50); const machineId = 'a'.repeat(31); // Just under 32 expect(validateToken(token, machineId)).toBe(false); }); it('should accept UUID with hyphens (strips them)', () => { const token = 'a'.repeat(50); const machineId = '12345678-1234-1234-1234-123456789abc'; // 36 chars with hyphens expect(validateToken(token, machineId)).toBe(true); }); it('should reject empty token', () => { const machineId = 'a'.repeat(32); expect(validateToken('', machineId)).toBe(false); }); it('should reject empty machineId', () => { const token = 'a'.repeat(50); expect(validateToken(token, '')).toBe(false); }); it('should reject non-hex characters in machineId', () => { const token = 'a'.repeat(50); const machineId = 'g'.repeat(32); // 'g' is not valid hex expect(validateToken(token, machineId)).toBe(false); }); }); describe('extractUserInfo', () => { it('should extract email and sub from valid JWT', () => { // JWT: {"email":"user@example.com","sub":"12345","exp":1234567890} const payload = Buffer.from( JSON.stringify({ email: 'user@example.com', sub: '12345', exp: 1234567890 }) ).toString('base64'); const token = `header.${payload}.signature`; const result = extractUserInfo(token); expect(result).toEqual({ email: 'user@example.com', userId: '12345', exp: 1234567890, }); }); it('should return undefined email when only sub claim exists', () => { // JWT: {"sub":"uuid-12345","exp":1234567890} const payload = Buffer.from(JSON.stringify({ sub: 'uuid-12345', exp: 1234567890 })).toString( 'base64' ); const token = `header.${payload}.signature`; const result = extractUserInfo(token); expect(result).toEqual({ email: undefined, userId: 'uuid-12345', exp: 1234567890, }); }); it('should return null for non-JWT token', () => { const token = 'a'.repeat(50); // Plain token const result = extractUserInfo(token); expect(result).toBe(null); }); it('should return null for malformed base64', () => { const token = 'header.!!!invalid-base64!!!.signature'; const result = extractUserInfo(token); expect(result).toBe(null); }); it('should handle JWT with user_id instead of sub', () => { // JWT: {"email":"user@example.com","user_id":"67890"} const payload = Buffer.from( JSON.stringify({ email: 'user@example.com', user_id: '67890' }) ).toString('base64'); const token = `header.${payload}.signature`; const result = extractUserInfo(token); expect(result).toEqual({ email: 'user@example.com', userId: '67890', exp: undefined, }); }); it('should return null for JWT with no meaningful claims', () => { // JWT: {"iat":1234567890} (only issued-at, no email/sub/exp) const payload = Buffer.from(JSON.stringify({ iat: 1234567890 })).toString('base64'); const token = `header.${payload}.signature`; const result = extractUserInfo(token); expect(result).toBe(null); }); }); describe('saveCredentials and loadCredentials', () => { it('should save and load credentials successfully', () => { const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'auto-detect', importedAt: new Date().toISOString(), }; saveCredentials(credentials); const loaded = loadCredentials(); expect(loaded).toEqual(credentials); }); it('should return null when no credentials file exists', () => { const loaded = loadCredentials(); expect(loaded).toBe(null); }); it('should create directory with restrictive permissions', () => { const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'manual', importedAt: new Date().toISOString(), }; saveCredentials(credentials); // CCS_HOME is set to tempDir, but getCcsDir() appends '.ccs' to it const credDir = path.join(tempDir, '.ccs', 'cursor'); expect(fs.existsSync(credDir)).toBe(true); // Check directory permissions (skip on Windows) if (process.platform !== 'win32') { const stats = fs.statSync(credDir); const mode = stats.mode & 0o777; expect(mode).toBe(0o700); } }); it('should return null for invalid JSON in credentials file', () => { // CCS_HOME is set to tempDir, getCcsDir() returns path.join(tempDir, '.ccs') const credDir = path.join(tempDir, '.ccs', 'cursor'); const credPath = path.join(credDir, 'credentials.json'); fs.mkdirSync(credDir, { recursive: true }); fs.writeFileSync(credPath, 'invalid json{{{'); const loaded = loadCredentials(); expect(loaded).toBe(null); }); it('should return null for credentials missing required fields', () => { const credDir = path.join(tempDir, '.ccs', 'cursor'); const credPath = path.join(credDir, 'credentials.json'); fs.mkdirSync(credDir, { recursive: true }); fs.writeFileSync( credPath, JSON.stringify({ accessToken: 'token', // Missing machineId, authMethod, importedAt }) ); const loaded = loadCredentials(); expect(loaded).toBe(null); }); it('should return null for credentials with wrong types', () => { const credDir = path.join(tempDir, '.ccs', 'cursor'); const credPath = path.join(credDir, 'credentials.json'); fs.mkdirSync(credDir, { recursive: true }); fs.writeFileSync( credPath, JSON.stringify({ accessToken: 123, // Wrong type (number instead of string) machineId: 'abc', authMethod: 'auto-detect', importedAt: new Date().toISOString(), }) ); const loaded = loadCredentials(); expect(loaded).toBe(null); }); it('should return null for invalid authMethod', () => { const credDir = path.join(tempDir, '.ccs', 'cursor'); const credPath = path.join(credDir, 'credentials.json'); fs.mkdirSync(credDir, { recursive: true }); fs.writeFileSync( credPath, JSON.stringify({ accessToken: 'token', machineId: 'abc', authMethod: 'invalid-method', // Invalid authMethod importedAt: new Date().toISOString(), }) ); const loaded = loadCredentials(); expect(loaded).toBe(null); }); }); describe('checkAuthStatus', () => { it('should return not authenticated when no credentials exist', () => { const status = checkAuthStatus(); expect(status.authenticated).toBe(false); expect(status.credentials).toBeUndefined(); }); it('should return authenticated for valid credentials', () => { const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'auto-detect', importedAt: new Date().toISOString(), }; saveCredentials(credentials); const status = checkAuthStatus(); expect(status.authenticated).toBe(true); expect(status.credentials).toEqual(credentials); expect(status.expired).toBe(false); expect(status.tokenAge).toBeDefined(); expect(status.tokenAge).toBeLessThan(1); // Just imported }); it('should detect expired credentials (importedAt > 24h ago)', () => { // Create credentials from 25 hours ago const past = new Date(); past.setHours(past.getHours() - 25); const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'manual', importedAt: past.toISOString(), }; saveCredentials(credentials); const status = checkAuthStatus(); expect(status.authenticated).toBe(true); expect(status.expired).toBe(true); expect(status.tokenAge).toBeGreaterThanOrEqual(24); }); it('should return not authenticated for invalid token format', () => { const credentials: CursorCredentials = { accessToken: 'short', // Invalid (too short) machineId: 'b'.repeat(32), authMethod: 'manual', importedAt: new Date().toISOString(), }; saveCredentials(credentials); const status = checkAuthStatus(); expect(status.authenticated).toBe(false); }); it('should use JWT exp claim when available', () => { // Create JWT token that expired 1 hour ago const expiredTime = Math.floor(Date.now() / 1000) - 3600; const payload = Buffer.from( JSON.stringify({ email: 'test@example.com', sub: '123', exp: expiredTime }) ).toString('base64'); const jwtToken = `header.${payload}.signature`; const credentials: CursorCredentials = { accessToken: jwtToken, machineId: 'b'.repeat(32), authMethod: 'auto-detect', importedAt: new Date().toISOString(), // Recent import }; saveCredentials(credentials); const status = checkAuthStatus(); expect(status.authenticated).toBe(true); expect(status.expired).toBe(true); // Should detect expiry from JWT exp }); it('should handle invalid importedAt date gracefully', () => { // Create credentials with valid format but garbage date value const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'manual', importedAt: 'invalid-date-garbage-2026-99-99T99:99:99Z', }; saveCredentials(credentials); const status = checkAuthStatus(); // Should still authenticate if token format is valid expect(status.authenticated).toBe(true); // tokenAge should be undefined due to invalid date (NaN from getTime()) expect(status.tokenAge).toBeUndefined(); // expired should be false (defaults to false when date parsing fails) expect(status.expired).toBe(false); }); }); describe('deleteCredentials', () => { it('should delete existing credentials file and return true', () => { const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'auto-detect', importedAt: new Date().toISOString(), }; saveCredentials(credentials); expect(loadCredentials()).not.toBe(null); const result = deleteCredentials(); expect(result).toBe(true); expect(loadCredentials()).toBe(null); }); it('should return false when credentials file does not exist', () => { const result = deleteCredentials(); expect(result).toBe(false); }); it('should handle multiple delete calls gracefully', () => { const credentials: CursorCredentials = { accessToken: 'a'.repeat(50), machineId: 'b'.repeat(32), authMethod: 'manual', importedAt: new Date().toISOString(), }; saveCredentials(credentials); // First delete should succeed expect(deleteCredentials()).toBe(true); // Second delete should return false (already deleted) expect(deleteCredentials()).toBe(false); }); }); describe('autoDetectTokens', () => { it('should return not found for Windows platform', () => { // Save original platform const originalPlatform = process.platform; // Mock Windows platform Object.defineProperty(process, 'platform', { value: 'win32', configurable: true, }); const result = autoDetectTokens(); expect(result.found).toBe(false); expect(result.error).toContain('not supported on Windows'); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true, }); }); it('should return not found when database file does not exist', () => { // Skip on Windows (already covered by previous test) if (process.platform === 'win32') { return; } const result = autoDetectTokens(); // Should fail because Cursor database doesn't exist in test environment expect(result.found).toBe(false); expect(result.error).toBeDefined(); }); it('should have found property in return type', () => { const result = autoDetectTokens(); // Verify return type structure expect(result).toHaveProperty('found'); expect(typeof result.found).toBe('boolean'); }); });