From d543c8339acd51a4cf8ade23e896249d3321fc1f Mon Sep 17 00:00:00 2001 From: Marat Boshernitsan Date: Tue, 19 Aug 2025 13:16:06 -0700 Subject: fix(core): harden user account caching (#6501) --- packages/core/src/utils/user_account.test.ts | 120 +++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 9 deletions(-) (limited to 'packages/core/src/utils/user_account.test.ts') diff --git a/packages/core/src/utils/user_account.test.ts b/packages/core/src/utils/user_account.test.ts index a0c861cc..35231ca3 100644 --- a/packages/core/src/utils/user_account.test.ts +++ b/packages/core/src/utils/user_account.test.ts @@ -99,18 +99,37 @@ describe('user_account', () => { it('should handle corrupted JSON by starting fresh', async () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), 'not valid json'); - const consoleDebugSpy = vi - .spyOn(console, 'debug') + const consoleLogSpy = vi + .spyOn(console, 'log') .mockImplementation(() => {}); await cacheGoogleAccount('test1@google.com'); - expect(consoleDebugSpy).toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalled(); expect(JSON.parse(fs.readFileSync(accountsFile(), 'utf-8'))).toEqual({ active: 'test1@google.com', old: [], }); }); + + it('should handle valid JSON with incorrect schema by starting fresh', async () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync( + accountsFile(), + JSON.stringify({ active: 'test1@google.com', old: 'not-an-array' }), + ); + const consoleLogSpy = vi + .spyOn(console, 'log') + .mockImplementation(() => {}); + + await cacheGoogleAccount('test2@google.com'); + + expect(consoleLogSpy).toHaveBeenCalled(); + expect(JSON.parse(fs.readFileSync(accountsFile(), 'utf-8'))).toEqual({ + active: 'test2@google.com', + old: [], + }); + }); }); describe('getCachedGoogleAccount', () => { @@ -139,14 +158,21 @@ describe('user_account', () => { it('should return null and log if file is corrupted', () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), '{ "active": "test@google.com"'); // Invalid JSON - const consoleDebugSpy = vi - .spyOn(console, 'debug') + const consoleLogSpy = vi + .spyOn(console, 'log') .mockImplementation(() => {}); const account = getCachedGoogleAccount(); expect(account).toBeNull(); - expect(consoleDebugSpy).toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalled(); + }); + + it('should return null if active key is missing', () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync(accountsFile(), JSON.stringify({ old: [] })); + const account = getCachedGoogleAccount(); + expect(account).toBeNull(); }); }); @@ -177,6 +203,56 @@ describe('user_account', () => { expect(stored.active).toBeNull(); expect(stored.old).toEqual([]); }); + + it('should handle corrupted JSON by creating a fresh file', async () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync(accountsFile(), 'not valid json'); + const consoleLogSpy = vi + .spyOn(console, 'log') + .mockImplementation(() => {}); + + await clearCachedGoogleAccount(); + + expect(consoleLogSpy).toHaveBeenCalled(); + const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); + expect(stored.active).toBeNull(); + expect(stored.old).toEqual([]); + }); + + it('should be idempotent if active account is already null', async () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync( + accountsFile(), + JSON.stringify({ active: null, old: ['old1@google.com'] }, null, 2), + ); + + await clearCachedGoogleAccount(); + + const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); + expect(stored.active).toBeNull(); + expect(stored.old).toEqual(['old1@google.com']); + }); + + it('should not add a duplicate to the old list', async () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync( + accountsFile(), + JSON.stringify( + { + active: 'active@google.com', + old: ['active@google.com'], + }, + null, + 2, + ), + ); + + await clearCachedGoogleAccount(); + + const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); + expect(stored.active).toBeNull(); + expect(stored.old).toEqual(['active@google.com']); + }); }); describe('getLifetimeGoogleAccounts', () => { @@ -193,12 +269,12 @@ describe('user_account', () => { it('should return 0 if the file is corrupted', () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), 'invalid json'); - const consoleDebugSpy = vi - .spyOn(console, 'debug') + const consoleLogSpy = vi + .spyOn(console, 'log') .mockImplementation(() => {}); expect(getLifetimeGoogleAccounts()).toBe(0); - expect(consoleDebugSpy).toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalled(); }); it('should return 1 if there is only an active account', () => { @@ -233,5 +309,31 @@ describe('user_account', () => { ); expect(getLifetimeGoogleAccounts()).toBe(3); }); + + it('should handle valid JSON with incorrect schema by returning 0', () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync( + accountsFile(), + JSON.stringify({ active: null, old: 1 }), + ); + const consoleLogSpy = vi + .spyOn(console, 'log') + .mockImplementation(() => {}); + + expect(getLifetimeGoogleAccounts()).toBe(0); + expect(consoleLogSpy).toHaveBeenCalled(); + }); + + it('should not double count if active account is also in old list', () => { + fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); + fs.writeFileSync( + accountsFile(), + JSON.stringify({ + active: 'test1@google.com', + old: ['test1@google.com', 'test2@google.com'], + }), + ); + expect(getLifetimeGoogleAccounts()).toBe(2); + }); }); }); -- cgit v1.2.3