/** * Unit Tests: Service Account Authentication * * Tests T033-T034: Test JWT authentication and credential validation * Tests the auth.js module in isolation * * @module tests/unit/auth */ import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; // ============================================================================= // T033: Unit test for Service Account JWT authentication // ============================================================================= describe('T033: Service Account JWT Authentication', () => { let originalEnv; beforeEach(() => { // Save original env originalEnv = process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); it('should create GoogleAuth client from GOOGLE_SERVICE_ACCOUNT_KEY env var', async () => { // Mock credentials as inline JSON (per clarification #1) const mockCredentials = { type: 'service_account', project_id: 'test-project', private_key_id: 'key123', private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n', client_email: 'test@test-project.iam.gserviceaccount.com', client_id: '123456789', auth_uri: 'https://accounts.google.com/o/oauth2/auth', token_uri: 'https://oauth2.googleapis.com/token', auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs' }; // Set env var with inline JSON process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(mockCredentials); // TODO: Import and call initializeAuth from src/auth.js // const { initializeAuth } = await import('../../src/auth.js'); // const auth = await initializeAuth(); // Verify GoogleAuth was created with correct credentials // assert.ok(auth, 'Should return auth client'); // assert.equal(auth.credentials.client_email, mockCredentials.client_email, 'Should use client_email from env var'); // Restore env if (originalEnv) { process.env.GOOGLE_SERVICE_ACCOUNT_KEY = originalEnv; } else { delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; } }); it('should use correct Drive API scope (read-only)', async () => { const mockCredentials = { type: 'service_account', project_id: 'test-project', private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n', client_email: 'test@test-project.iam.gserviceaccount.com' }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(mockCredentials); // TODO: Import and call initializeAuth // const { initializeAuth } = await import('../../src/auth.js'); // const auth = await initializeAuth(); // Verify scope is read-only const expectedScope = 'https://www.googleapis.com/auth/drive.readonly'; // assert.ok(auth.scopes.includes(expectedScope), 'Should use drive.readonly scope'); // Restore env if (originalEnv) { process.env.GOOGLE_SERVICE_ACCOUNT_KEY = originalEnv; } else { delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; } }); it('should parse inline JSON from env var correctly', async () => { // Test with different JSON formatting (whitespace, escaped quotes) const mockCredentials = { client_email: 'test@project.iam.gserviceaccount.com', private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n', project_id: 'test-project' }; // Set with extra whitespace process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(mockCredentials, null, 2); // TODO: Import and call initializeAuth // const { initializeAuth } = await import('../../src/auth.js'); // const auth = await initializeAuth(); // Should parse correctly despite formatting // assert.ok(auth, 'Should parse JSON with whitespace'); // Restore env if (originalEnv) { process.env.GOOGLE_SERVICE_ACCOUNT_KEY = originalEnv; } else { delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; } }); }); // ============================================================================= // T034: Unit test for credential validation // ============================================================================= describe('T034: Credential Validation', () => { it('should detect missing client_email field', async () => { const invalidCredentials = { private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n', project_id: 'test-project' // Missing client_email }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(invalidCredentials); // TODO: Import validateCredentials from src/auth.js // const { validateCredentials } = await import('../../src/auth.js'); // Should throw error for missing client_email // await assert.rejects( // async () => await validateCredentials(invalidCredentials), // { message: /client_email/ }, // 'Should reject credentials without client_email' // ); delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); it('should detect missing private_key field', async () => { const invalidCredentials = { client_email: 'test@project.iam.gserviceaccount.com', project_id: 'test-project' // Missing private_key }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(invalidCredentials); // TODO: Import validateCredentials // const { validateCredentials } = await import('../../src/auth.js'); // Should throw error for missing private_key // await assert.rejects( // async () => await validateCredentials(invalidCredentials), // { message: /private_key/ }, // 'Should reject credentials without private_key' // ); delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); it('should detect missing project_id field', async () => { const invalidCredentials = { client_email: 'test@project.iam.gserviceaccount.com', private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n' // Missing project_id }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(invalidCredentials); // TODO: Import validateCredentials // const { validateCredentials } = await import('../../src/auth.js'); // Should throw error for missing project_id // await assert.rejects( // async () => await validateCredentials(invalidCredentials), // { message: /project_id/ }, // 'Should reject credentials without project_id' // ); delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); it('should detect empty credential fields', async () => { const invalidCredentials = { client_email: '', // Empty private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n', project_id: 'test-project' }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(invalidCredentials); // TODO: Import validateCredentials // const { validateCredentials } = await import('../../src/auth.js'); // Should throw error for empty client_email // await assert.rejects( // async () => await validateCredentials(invalidCredentials), // { message: /client_email.*empty/ }, // 'Should reject empty client_email' // ); delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); it('should accept valid credentials', async () => { const validCredentials = { type: 'service_account', project_id: 'test-project', private_key: '-----BEGIN PRIVATE KEY-----\nMOCK_KEY\n-----END PRIVATE KEY-----\n', client_email: 'test@test-project.iam.gserviceaccount.com' }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(validCredentials); // TODO: Import validateCredentials // const { validateCredentials } = await import('../../src/auth.js'); // Should not throw for valid credentials // await assert.doesNotReject( // async () => await validateCredentials(validCredentials), // 'Should accept valid credentials' // ); delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); it('should trigger fatal error handler on invalid credentials (exit code 1)', async () => { // Per T016: Fatal error handler should log to stderr and exit with code 1 const invalidCredentials = { invalid: 'structure' }; process.env.GOOGLE_SERVICE_ACCOUNT_KEY = JSON.stringify(invalidCredentials); // TODO: Import initializeAuth which should call fatal error handler // const { initializeAuth } = await import('../../src/auth.js'); // Mock process.exit to prevent actual exit // let exitCode; // const originalExit = process.exit; // process.exit = (code) => { exitCode = code; throw new Error('EXIT'); }; // try { // await initializeAuth(); // } catch (e) { // if (e.message === 'EXIT') { // assert.equal(exitCode, 1, 'Should exit with code 1 on invalid credentials'); // } else { // throw e; // } // } finally { // process.exit = originalExit; // } delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY; }); });