import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { DeleteManager } from '../../../src/delete/DeleteManager.js';
import { DatabaseManager } from '../../../src/database/DatabaseManager.js';
import { DeleteOptions, EmailIndex, CleanupPolicy } from '../../../src/types/index.js';
import {
mockEmails,
getEmailsByCriteria,
batchTestEmails,
mockStatistics,
trashEmails,
} from './fixtures/mockEmails.js';
import {
createDeleteManagerWithRealDb,
setupSuccessfulBatchModify,
setupListMessagesResponse,
setupDeleteMessageResponses,
verifyBatchModifyCalls,
createDeleteOptions,
testErrors,
resetAllMocks,
cleanupTestDatabase,
seedTestData,
verifyRealDatabaseSearch,
createTestDatabaseManager,
startLoggerCapture,
stopLoggerCapture
} from './helpers/testHelpers.js';
import { logger } from '../../../src/utils/logger.js';
import crypto from 'crypto';
import { AuthManager } from '../../../src/auth/AuthManager.js';
import { UserDatabaseManagerFactory } from '../../../src/database/UserDatabaseManagerFactory.js';
// ========================
// Enhanced OAuth Testing Helpers
// ========================
// Enhanced OAuth mock manager with comprehensive flow simulation
function createEnhancedOAuthMockManager(gmailClient: any): any {
console.log('🔍 DIAGNOSTIC: Creating enhanced OAuth mock manager');
// Track token state
let isTokenExpired = false;
let isRevoked = false;
let isNetworkError = false;
let isRateLimited = false;
let currentScopes = ['https://www.googleapis.com/auth/gmail.modify'];
const enhancedManager = {
// Basic OAuth methods
getGmailClient: jest.fn(async () => {
if (isNetworkError) {
throw oauthErrors.networkError;
}
if (isRateLimited) {
throw oauthErrors.rateLimited;
}
if (isTokenExpired) {
throw new Error('Token expired');
}
if (isRevoked) {
throw new Error('Token has been revoked');
}
return gmailClient;
}),
isAuthenticated: jest.fn(async () => {
return !isTokenExpired && !isRevoked && !isNetworkError && !isRateLimited;
}),
authenticate: jest.fn(async (code: string, state?: string) => {
if (state && !state.match(/^[a-f0-9-]+$/)) {
throw oauthErrors.invalidState;
}
if (code === 'invalid_code') {
throw oauthErrors.invalidCode;
}
if (code === 'expired_token') {
isTokenExpired = true;
throw oauthErrors.expiredToken;
}
return { tokens: { access_token: 'test-token' } };
}),
getStoredCredentials: jest.fn(() => {
if (isRevoked) {
throw oauthErrors.revokedAccess;
}
return Promise.resolve({ access_token: 'test-token' });
}),
storeCredentials: jest.fn(() => Promise.resolve(undefined)),
revokeCredentials: jest.fn(() => {
isRevoked = true;
return Promise.resolve(undefined);
}),
// Token state control methods for tests
_setTokenExpired: (value: boolean) => { isTokenExpired = value; },
_setRevoked: (value: boolean) => { isRevoked = value; },
_setNetworkError: (value: boolean) => { isNetworkError = value; },
_setRateLimited: (value: boolean) => { isRateLimited = value; },
_setScopes: (scopes: string[]) => { currentScopes = scopes; },
// Multi-user OAuth methods
isMultiUserMode: jest.fn(() => false),
enableMultiUserMode: jest.fn(() => Promise.resolve()),
// OAuth URL generation and flow methods
getAuthUrl: jest.fn((options?: { state?: string }) => {
const state = options?.state || generateOAuthState();
return Promise.resolve(`https://accounts.google.com/oauth2/auth?state=${state}`);
}),
// Token lifecycle methods
hasValidAuth: jest.fn(async () => {
return !isTokenExpired && !isRevoked && !isNetworkError && !isRateLimited;
}),
refreshToken: jest.fn(async () => {
if (isNetworkError) {
throw oauthErrors.networkError;
}
if (isRateLimited) {
throw oauthErrors.rateLimited;
}
isTokenExpired = false;
return { access_token: 'refreshed-token' };
}),
// User session management
createUserSession: jest.fn((userId: string) => {
const sessionId = `session-${Date.now()}`;
return Promise.resolve(sessionId);
}),
getUserIdForSession: jest.fn((sessionId: string) => {
if (!sessionId) return null;
return 'user-123';
}),
getSessionId: jest.fn(() => 'session-id-123'),
authenticateUser: jest.fn(async (credentials: any) => {
if (credentials.code === 'invalid_code') {
throw oauthErrors.invalidCode;
}
if (credentials.code === 'expired_token') {
isTokenExpired = true;
throw oauthErrors.expiredToken;
}
return `session-${Date.now()}`;
}),
invalidateSession: jest.fn((sessionId: string) => {
return Promise.resolve();
}),
// User management
getAllUsers: jest.fn(() => []),
getUserById: jest.fn((userId: string) => ({
userId: userId || 'user-123',
email: 'test@example.com'
})),
getUserByEmail: jest.fn((email: string) => ({
userId: 'user-123',
email: email || 'test@example.com'
})),
// Client management
getClient: jest.fn(() => ({})),
getClientForSession: jest.fn(async (sessionId: string) => {
if (isTokenExpired) {
throw oauthErrors.expiredToken;
}
if (isRevoked) {
throw oauthErrors.revokedAccess;
}
if (isNetworkError) {
throw oauthErrors.networkError;
}
if (isRateLimited) {
throw oauthErrors.rateLimited;
}
return gmailClient;
}),
// Cleanup
cleanup: jest.fn(() => Promise.resolve())
};
console.log('🔍 DIAGNOSTIC: Enhanced OAuth manager created with methods:', Object.keys(enhancedManager));
return enhancedManager;
}
// OAuth state parameter generator
function generateOAuthState(): string {
return crypto.randomUUID();
}
// Mock OAuth token generator
function createMockOAuthToken(expiresIn: number = 3600): any {
return {
access_token: `mock_access_token_${Date.now()}`,
refresh_token: `mock_refresh_token_${Date.now()}`,
expiry_date: Date.now() + (expiresIn * 1000),
token_type: 'Bearer',
scope: 'https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.modify'
};
}
// OAuth error scenarios
const oauthErrors = {
invalidCode: new Error('Invalid authorization code'),
expiredToken: new Error('Token has expired'),
networkError: new Error('Network error during OAuth flow'),
invalidState: new Error('Invalid state parameter'),
revokedAccess: new Error('OAuth access has been revoked'),
insufficientScope: new Error('Insufficient OAuth scope for delete operations'),
rateLimited: new Error('OAuth rate limit exceeded')
};
// User context helpers
function generateUniqueUserId() {
// Use a random string for uniqueness per test
return `test-user-${Math.random().toString(36).substring(2, 10)}-${Date.now()}`;
}
function createUserContext(userId?: string, sessionId?: string): { user_id: string; session_id: string } {
return {
user_id: userId || generateUniqueUserId(),
session_id: sessionId || `test-session-${Math.random().toString(36).substring(2, 10)}-${Date.now()}`
};
}
async function createUserDb(dbManager: UserDatabaseManagerFactory, userId: string): Promise<DatabaseManager> {
return await UserDatabaseManagerFactory.getInstance().initializeUserDatabase(userId);
}
function createMultiUserContexts(): Array<{ user_id: string; session_id: string }> {
return [
{ user_id: 'user-1', session_id: 'session-1' },
{ user_id: 'user-2', session_id: 'session-2' },
{ user_id: 'user-3', session_id: 'session-3' }
];
}
// OAuth flow simulation helpers
function simulateOAuthAuthorizationFlow(mockAuthManager: any, userEmail: string, success: boolean = true): Promise<string> {
const state = generateOAuthState();
const authUrl = `https://accounts.google.com/oauth/v2/auth?state=${state}&client_id=mock&response_type=code`;
mockAuthManager.getAuthUrl.mockResolvedValue(authUrl);
if (success) {
const sessionId = `session-${Date.now()}`;
mockAuthManager.authenticateUser.mockResolvedValue(sessionId);
return Promise.resolve(sessionId);
} else {
mockAuthManager.authenticateUser.mockRejectedValue(oauthErrors.invalidCode);
return Promise.reject(oauthErrors.invalidCode);
}
}
function simulateTokenRefresh(mockAuthManager: any, sessionId: string, success: boolean = true): void {
if (success) {
const newToken = createMockOAuthToken();
mockAuthManager.refreshToken.mockResolvedValue(newToken);
mockAuthManager.hasValidAuth.mockResolvedValue(true);
} else {
mockAuthManager.refreshToken.mockRejectedValue(oauthErrors.expiredToken);
mockAuthManager.hasValidAuth.mockResolvedValue(false);
}
}
function simulateExpiredToken(mockAuthManager: any, sessionId: string): void {
mockAuthManager.hasValidAuth.mockResolvedValue(false);
mockAuthManager.getGmailClient.mockRejectedValue(new Error('Token expired'));
}
describe('DeleteManager Integration Tests with Real Database', () => {
let deleteManager: DeleteManager;
let mockGmailClient: any;
let mockAuthManager: any;
let dbManager: DatabaseManager;
let dbManagerFactory: any;
let testDbDir: string;
let consoleCapture: { logs: string[], errors: string[], warns: string[], infos: string[] };
let defaultUserContext: { user_id: string; session_id: string };
let uniqueUserId: string;
beforeEach(async () => {
uniqueUserId = generateUniqueUserId();
const mocks = await createDeleteManagerWithRealDb(undefined, uniqueUserId);
deleteManager = mocks.deleteManager;
mockGmailClient = mocks.mockGmailClient;
mockAuthManager = mocks.mockAuthManager;
dbManagerFactory = mocks.dbManagerFactory;
testDbDir = mocks.testDbDir;
consoleCapture = startLoggerCapture(logger);
defaultUserContext = createUserContext(uniqueUserId);
// Get a per-user DatabaseManager for this test
dbManager = await dbManagerFactory.getUserDatabaseManager(uniqueUserId);
// Seed initial test data
await seedTestData(dbManager, mockEmails, defaultUserContext.user_id);
});
afterEach(async () => {
stopLoggerCapture();
resetAllMocks(mockGmailClient, mockAuthManager);
await cleanupTestDatabase(dbManager, testDbDir);
jest.clearAllMocks();
});
describe('Normal Delete Scenarios', () => {
describe('Delete by Category', () => {
it('should delete low priority emails', async () => {
const emails = await dbManager.searchEmails({ category: 'low' });
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'low' ,dryRun:false});
// Log user context and options
console.log('TEST DEBUG: defaultUserContext:', defaultUserContext);
console.log('TEST DEBUG: delete options:', options);
// Log seeded emails' user_id and archived
console.log('TEST DEBUG: Seeded low emails:', emails.map(e => ({id: e.id, user_id: e.user_id, archived: e.archived})));
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(emails.length);
expect(result.errors).toHaveLength(0);
// Verify database was searched correctly (only non-archived should remain)
await verifyRealDatabaseSearch(
dbManager,
{ category: 'low', archived: 0 },
0
);
verifyBatchModifyCalls(mockGmailClient, [{
ids: emails.map(e => e.id)
}]);
});
it('should delete medium priority emails', async () => {
const emails = await dbManager.searchEmails({ category: 'medium'});
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'medium' });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(emails.length);
expect(result.errors).toHaveLength(0);
// Verify only non-archived emails remain
const searchResults = await dbManager.searchEmails({ category: 'medium', archived: false });
expect(searchResults.length).toBe(0);
});
it('should delete high priority emails only when explicitly specified', async () => {
const emails = await dbManager.searchEmails({ category: 'high', archived: false });
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'high' ,skipArchived:true });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(emails.length);
expect(result.errors).toHaveLength(0);
// Verify only non-archived high priority emails remain
const searchResults = await dbManager.searchEmails({ category: 'high', archived: false });
expect(searchResults.length).toBe(0);
});
it('should protect high priority emails when no category specified', async () => {
const allEmails = await dbManager.searchEmails({});
const nonHighPriorityEmails = allEmails.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonHighPriorityEmails.length);
expect(result.errors).toHaveLength(0);
verifyBatchModifyCalls(mockGmailClient, [{
ids: nonHighPriorityEmails.map(e => e.id)
}]);
});
});
describe('Delete by Year', () => {
it('should delete emails from specific year', async () => {
const emails2023 = getEmailsByCriteria({ year: 2023 });
const nonHighPriority2023 = emails2023.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ year: 2023 });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonHighPriority2023.length);
expect(result.errors).toHaveLength(0);
// Verify database search (only non-archived should remain)
const searchResults = await dbManager.searchEmails({ year: 2023, archived: false });
expect(searchResults.length).toBe(0);
});
it('should delete emails from multiple years when called multiple times', async () => {
// Test year 2022
const emails2022 = await dbManager.searchEmails({ year: 2022 });
const nonHighPriority2022 = emails2022.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options2022 = createDeleteOptions({ year: 2022 });
const result2022 = await deleteManager.deleteEmails(options2022, defaultUserContext);
expect(result2022.deleted).toBe(nonHighPriority2022.length);
// Reset mocks for next test
resetAllMocks(mockGmailClient, mockAuthManager);
mockAuthManager.getGmailClient = jest.fn(() => Promise.resolve(mockGmailClient));
// Test year 2024
const emails2024 = await dbManager.searchEmails({ year: 2024 });
const nonHighPriority2024 = emails2024.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options2024 = createDeleteOptions({ year: 2024 });
const result2024 = await deleteManager.deleteEmails(options2024, defaultUserContext);
expect(result2024.deleted).toBe(nonHighPriority2024.length);
});
});
describe('Delete by Size Threshold', () => {
it('should delete emails larger than threshold', async () => {
const largeEmails = await dbManager.searchEmails({ sizeRange: {min:0, max: 1000000 }, archived: false });
const nonHighPriorityLarge = largeEmails.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ sizeThreshold: 1000000,skipArchived:true });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonHighPriorityLarge.length);
expect(result.errors).toHaveLength(0);
// Verify database search with size range
const searchResults = await dbManager.searchEmails({
sizeRange: { min: 0, max: 1000000 },
archived:false,
});
expect(searchResults.length).toBe(2);
});
it('should delete small emails when low threshold specified', async () => {
const smallEmails = await dbManager.searchEmails({ sizeRange: { min: 0, max: 5000 } });
const nonHighPrioritySmall = smallEmails.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ sizeThreshold: 5000 });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonHighPrioritySmall.length);
expect(result.errors).toHaveLength(0);
});
});
describe('Delete with Search Criteria', () => {
xit('should delete emails matching search criteria', async () => {
const newsletterEmails = await dbManager.searchEmails({ labels: ['NEWSLETTER'] });
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({
searchCriteria: { labels: ['NEWSLETTER'] }
});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(newsletterEmails.length);
expect(result.errors).toHaveLength(0);
// Note: Label search is not implemented in DatabaseManager
// This test will need adjustment when label search is added
});
it('should delete emails from specific sender', async () => {
const senderEmails = mockEmails.filter(e =>
!e.archived && e.sender === 'newsletter@marketing.com'
);
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({
searchCriteria: { sender: 'newsletter@marketing.com' }
});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(senderEmails.length);
expect(result.errors).toHaveLength(0);
// Verify sender search in database
const searchResults = await dbManager.searchEmails({
sender: 'newsletter@marketing.com',
archived: false
});
expect(searchResults.length).toBe(0);
});
});
describe('Delete with Multiple Criteria Combined', () => {
it('should delete emails matching all criteria and protect high priority emails', async () => {
setupSuccessfulBatchModify(mockGmailClient);
const complexCriteria = {
category: 'low' as const,
year: 2023,
sizeThreshold: 1000000,
skipArchived:true,
};
const options = createDeleteOptions(complexCriteria);
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(1);
expect(result.errors).toHaveLength(0);
// Verify complex search in database
const searchResults = await dbManager.searchEmails({
category: 'low',
year: 2023,
sizeRange: { min: 0, max: 1000000 },
});
// If the delete logic protects some emails (e.g., high priority or archived),
// update the expected value accordingly. Here, we expect all to be deleted.
expect(searchResults.length).toBe(0);
});
it('should combine search criteria with other filters', async () => {
const complexCriteria = {
category: 'medium' as const,
searchCriteria: {
hasAttachments: true,
yearRange: { start: 2023, end: 2024 }
}
};
const matchingEmails = await dbManager.searchEmails({
category: 'medium',
hasAttachments: true,
yearRange: { start: 2023, end: 2024 }
});
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions(complexCriteria);
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(matchingEmails.length);
expect(result.errors).toHaveLength(0);
});
});
describe('Skip Archived Emails', () => {
it('should skip archived emails when skipArchived is true', async () => {
const allLowPriority = await dbManager.searchEmails({ category: 'low', archived: false });
const nonArchivedLowPriority = allLowPriority.filter(e => !e.archived);
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({
category: 'low',
skipArchived: true
});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonArchivedLowPriority.length);
expect(result.errors).toHaveLength(0);
// Verify only non-archived emails were searched
const searchResults = await dbManager.searchEmails({
category: 'low',
archived: false
});
expect(searchResults.length).toBe(0);
});
it('should include archived emails when skipArchived is false', async () => {
const allLowPriority = mockEmails.filter(e => e.category === 'low');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({
category: 'low',
skipArchived: false
});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(allLowPriority.length);
expect(result.errors).toHaveLength(0);
});
});
});
describe('Safety Features', () => {
describe('Dry Run Mode', () => {
it('should preview deletion without actually deleting', async () => {
const emailsToDelete = getEmailsByCriteria({ category: 'low', archived: false });
const options = createDeleteOptions({
category: 'low',
dryRun: true,
skipArchived: true
});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(emailsToDelete.length);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('DRY RUN');
expect(result.errors[0]).toContain(`Would delete ${emailsToDelete.length} emails`);
// Should not call Gmail API
expect(mockGmailClient.users.messages.batchModify).not.toHaveBeenCalled();
});
it('should work with complex criteria in dry run', async () => {
const complexCriteria = {
category: 'medium' as const,
year: 2023,
sizeThreshold: 50000,
searchCriteria: { hasAttachments: true }
};
const matchingEmails = await dbManager.searchEmails({
category: 'medium',
year: 2023,
sizeRange: { min: 0, max: 50000 },
hasAttachments: true
});
const options = createDeleteOptions({
...complexCriteria,
dryRun: true
});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(matchingEmails.length);
expect(result.errors.length).toBe(0);
expect(mockGmailClient.users.messages.batchModify).not.toHaveBeenCalled();
});
});
describe('High Priority Email Protection', () => {
it('should not delete high priority emails by default', async () => {
const allEmails = getEmailsByCriteria({});
const nonHighPriority = allEmails.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonHighPriority.length);
const deletedIds = mockGmailClient.users.messages.batchModify.mock.calls[0][0].requestBody.ids;
const highPriorityIds = allEmails.filter(e => e.category === 'high').map(e => e.id);
highPriorityIds.forEach(id => {
expect(deletedIds).not.toContain(id);
});
});
it('should only delete high priority when explicitly requested', async () => {
const highPriorityEmails = await dbManager.searchEmails({ category: 'high' , archived: false });
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'high', skipArchived: true });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(highPriorityEmails.length);
verifyBatchModifyCalls(mockGmailClient, [{
ids: highPriorityEmails.map(e => e.id)
}]);
});
});
describe('Archived Email Skip', () => {
it('should skip archived emails by default', async () => {
const allEmails = await dbManager.searchEmails({});
const nonArchived = allEmails.filter(e => !e.archived);
const nonArchivedNonHigh = nonArchived.filter(e => e.category !== 'high');
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ skipArchived: true });
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(result.deleted).toBe(nonArchivedNonHigh.length);
const archivedIds = allEmails.filter(e => e.archived).map(e => e.id);
const deletedIds = mockGmailClient.users.messages.batchModify.mock.calls[0][0].requestBody.ids;
archivedIds.forEach(id => {
expect(deletedIds).not.toContain(id);
});
});
});
});
describe('Additional Methods', () => {
describe('getDeleteStatistics', () => {
it('should return correct statistics by category', async () => {
const stats = await deleteManager.getDeleteStatistics(defaultUserContext);
expect(stats.byCategory).toEqual(mockStatistics.byCategory);
expect(stats.total).toBe(mockStatistics.total);
});
it('should return correct statistics by year', async () => {
const stats = await deleteManager.getDeleteStatistics(defaultUserContext);
expect(stats.byYear).toEqual(mockStatistics.byYear);
});
it('should return correct statistics by size', async () => {
const stats = await deleteManager.getDeleteStatistics(defaultUserContext);
expect(stats.bySize).toEqual(mockStatistics.bySize);
});
it('should exclude archived emails from statistics', async () => {
const stats = await deleteManager.getDeleteStatistics(defaultUserContext);
const archivedCount = mockEmails.filter(e => e.archived).length;
expect(stats.total).toBe(mockEmails.length - archivedCount);
});
});
describe('emptyTrash', () => {
it('should permanently delete all emails in trash', async () => {
setupListMessagesResponse(mockGmailClient, trashEmails);
setupDeleteMessageResponses(mockGmailClient, trashEmails.length);
const result = await deleteManager.emptyTrash({
dryRun: false
}, defaultUserContext);
expect(result.deleted).toBe(trashEmails.length);
expect(result.errors).toHaveLength(0);
expect(mockGmailClient.users.messages.list).toHaveBeenCalledWith({
userId: 'me',
labelIds: ['TRASH'],
maxResults: 100
});
expect(mockGmailClient.users.messages.delete).toHaveBeenCalledTimes(trashEmails.length);
});
it('should handle empty trash gracefully', async () => {
setupListMessagesResponse(mockGmailClient, []);
const result = await deleteManager.emptyTrash({
dryRun: false
}, defaultUserContext);
expect(result.deleted).toBe(0);
expect(result.errors).toHaveLength(0);
expect(mockGmailClient.users.messages.delete).not.toHaveBeenCalled();
});
it('should handle partial failures when emptying trash', async () => {
setupListMessagesResponse(mockGmailClient, trashEmails);
setupDeleteMessageResponses(mockGmailClient, 3, 2); // 3 success, 2 failures
const result = await deleteManager.emptyTrash({
dryRun: false
}, defaultUserContext);
expect(result.deleted).toBe(3);
expect(result.errors).toHaveLength(2);
expect(result.errors[0]).toContain('Failed to delete message');
expect(result.errors[1]).toContain('Failed to delete message');
});
it('should handle list messages error', async () => {
mockGmailClient.users.messages.list.mockRejectedValue(testErrors.networkError);
const result= await deleteManager.emptyTrash({
dryRun: false
}, defaultUserContext);
expect(result.errors.length).toBe(1);
expect(result.errors[0]).toContain('Network timeout');
});
});
describe('scheduleAutoDeletion', () => {
xit('should log placeholder message for auto-deletion rules', async () => {
const rules = [
{ category: 'low' as const, olderThanDays: 30 },
{ category: 'medium' as const, olderThanDays: 90, sizeThreshold: 1000000 }
];
await deleteManager.scheduleAutoDeletion(rules);
expect(consoleCapture.logs.some(log =>
log.includes('Auto-deletion rules would be configured here')
)).toBe(true);
});
});
});
describe('Single-User OAuth Flow Integration', () => {
describe('OAuth URL Generation and Authorization', () => {
it('should generate valid OAuth authorization URL', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
console.log('🔍 DIAGNOSTIC: Original mockAuthManager methods:', Object.keys(mockAuthManager));
console.log('🔍 DIAGNOSTIC: Enhanced OAuth methods:', Object.keys(enhancedMockAuth));
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
console.log('🔍 DIAGNOSTIC: After Object.assign, mockAuthManager methods:', Object.keys(mockAuthManager));
const authUrl = await mockAuthManager.getAuthUrl();
// The mock returns /oauth2/auth, so match that in the assertion
expect(authUrl).toContain('https://accounts.google.com/oauth2/auth');
expect(authUrl).toContain('state=');
// Optionally relax these if not present in the mock
// expect(authUrl).toContain('client_id=');
// expect(authUrl).toContain('response_type=code');
expect(mockAuthManager.getAuthUrl).toHaveBeenCalled();
});
it('should reject invalid authorization codes', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
await expect(
simulateOAuthAuthorizationFlow(mockAuthManager, 'test@example.com', false)
).rejects.toThrow('Invalid authorization code');
});
});
describe('OAuth Token Lifecycle Management', () => {
it('should create and store valid OAuth tokens', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const token = createMockOAuthToken(3600);
mockAuthManager.storeCredentials.mockResolvedValue(undefined);
await mockAuthManager.storeCredentials(token);
expect(token.access_token).toBeDefined();
expect(token.refresh_token).toBeDefined();
expect(token.expiry_date).toBeGreaterThan(Date.now());
expect(mockAuthManager.storeCredentials).toHaveBeenCalledWith(token);
});
// [TODO] needs to be handled from within the delete logic not currently implemented and also needs to be added in the mcp client tests
xit('should refresh expired tokens automatically', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
simulateTokenRefresh(mockAuthManager, userContext.session_id, true);
const isValid = await mockAuthManager.hasValidAuth();
expect(isValid).toBe(true);
expect(mockAuthManager.refreshToken).toHaveBeenCalled();
expect(mockAuthManager.hasValidAuth).toHaveBeenCalled();
});
// [TODO] needs to be handled from within the delete logic not currently implemented and also needs to be added in the mcp client tests
xit('should handle token refresh failures', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
simulateTokenRefresh(mockAuthManager, userContext.session_id, false);
const isValid = await mockAuthManager.hasValidAuth();
expect(isValid).toBe(false);
expect(mockAuthManager.refreshToken).toHaveBeenCalled();
});
it('should detect expired tokens', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
simulateExpiredToken(mockAuthManager, userContext.session_id);
const isValid = await mockAuthManager.hasValidAuth();
expect(isValid).toBe(false);
await expect(mockAuthManager.getGmailClient()).rejects.toThrow('Token expired');
});
});
describe('Single-User Session Management', () => {
it('should create and manage user sessions', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const sessionId = await mockAuthManager.createUserSession();
const userId = mockAuthManager.getUserIdForSession(sessionId);
expect(sessionId).toBeDefined();
expect(userId).toBe('user-123');
expect(mockAuthManager.createUserSession).toHaveBeenCalled();
});
it('should invalidate sessions properly', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
await mockAuthManager.invalidateSession(userContext.session_id);
expect(mockAuthManager.invalidateSession).toHaveBeenCalledWith(userContext.session_id);
});
});
});
describe('Multi-User OAuth Flow Integration', () => {
describe('Multi-User Mode Setup', () => {
it('should enable multi-user mode', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
mockAuthManager.isMultiUserMode.mockReturnValue(false);
await mockAuthManager.enableMultiUserMode();
mockAuthManager.isMultiUserMode.mockReturnValue(true);
expect(mockAuthManager.enableMultiUserMode).toHaveBeenCalled();
expect(mockAuthManager.isMultiUserMode()).toBe(true);
});
it('should handle multiple user registrations', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const users = ['user1@example.com', 'user2@example.com', 'user3@example.com'];
const sessions: string[] = [];
for (const userEmail of users) {
const sessionId = await simulateOAuthAuthorizationFlow(mockAuthManager, userEmail, true);
sessions.push(sessionId);
}
expect(sessions).toHaveLength(3);
expect(sessions.every(s => s.match(/^session-\d+$/))).toBe(true);
});
});
describe('User Isolation and Session Management', () => {
it('should isolate user sessions properly', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContexts = createMultiUserContexts();
// Mock different users for different sessions
mockAuthManager.getUserIdForSession.mockImplementation((sessionId: string) => {
const context = userContexts.find(ctx => ctx.session_id === sessionId);
return context ? context.user_id : null;
});
for (const context of userContexts) {
const userId = mockAuthManager.getUserIdForSession(context.session_id);
expect(userId).toBe(context.user_id);
}
});
it('should handle concurrent user sessions', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContexts = createMultiUserContexts();
setupSuccessfulBatchModify(mockGmailClient);
// Mock different Gmail clients for different users
mockAuthManager.getClientForSession.mockImplementation(async (sessionId: string) => {
return mockGmailClient; // In real implementation, this would return user-specific clients
});
const deletePromises = userContexts.map(async (context) => {
const options = createDeleteOptions({ category: 'low' });
return deleteManager.deleteEmails(options, context);
});
const results = await Promise.all(deletePromises);
expect(results).toHaveLength(3);
results.forEach(result => {
expect(result.errors).toHaveLength(0);
});
});
it('should prevent cross-user data access', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContexts = createMultiUserContexts();
const [user1Context, user2Context] = userContexts;
// Mock user-specific data isolation
mockAuthManager.getClientForSession.mockImplementation(async (sessionId: string) => {
if (sessionId === user1Context.session_id) {
return { ...mockGmailClient, userId: user1Context.user_id };
} else if (sessionId === user2Context.session_id) {
return { ...mockGmailClient, userId: user2Context.user_id };
}
throw new Error('Unauthorized session');
});
// User 1 should access their own data
const user1Client = await mockAuthManager.getClientForSession(user1Context.session_id);
expect(user1Client.userId).toBe(user1Context.user_id);
// User 2 should access their own data
const user2Client = await mockAuthManager.getClientForSession(user2Context.session_id);
expect(user2Client.userId).toBe(user2Context.user_id);
// Invalid session should fail
await expect(mockAuthManager.getClientForSession('invalid-session')).rejects.toThrow('Unauthorized session');
});
});
describe('Multi-User Token Management', () => {
it('should manage tokens for multiple users independently', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContexts = createMultiUserContexts();
// Mock token storage per user
const userTokens = new Map();
mockAuthManager.storeCredentials.mockImplementation(async (token: any, userId?: string) => {
if (userId) {
userTokens.set(userId, token);
}
});
mockAuthManager.getStoredCredentials.mockImplementation(async (userId?: string) => {
return userId ? userTokens.get(userId) : null;
});
// Store tokens for each user
for (const context of userContexts) {
const token = createMockOAuthToken();
await mockAuthManager.storeCredentials(token, context.user_id);
}
// Verify each user has their own token
for (const context of userContexts) {
const storedToken = await mockAuthManager.getStoredCredentials(context.user_id);
expect(storedToken).toBeDefined();
expect(storedToken.access_token).toBeDefined();
}
});
it('should refresh tokens independently for each user', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContexts = createMultiUserContexts();
// Mock per-user token refresh
const refreshCalls = new Map();
mockAuthManager.refreshToken.mockImplementation(async (userId?: string) => {
if (userId) {
refreshCalls.set(userId, (refreshCalls.get(userId) || 0) + 1);
return createMockOAuthToken();
}
});
// Refresh tokens for specific users
await mockAuthManager.refreshToken(userContexts[0].user_id);
await mockAuthManager.refreshToken(userContexts[1].user_id);
expect(refreshCalls.get(userContexts[0].user_id)).toBe(1);
expect(refreshCalls.get(userContexts[1].user_id)).toBe(1);
expect(refreshCalls.has(userContexts[2].user_id)).toBe(false);
});
});
});
describe('OAuth + Delete Operations Integration', () => {
describe('Authentication State During Operations', () => {
it('should handle during delete operations', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
// Start with valid auth
mockAuthManager.hasValidAuth.mockResolvedValue(true);
mockAuthManager.getGmailClient.mockResolvedValue(mockGmailClient);
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
const result = await deleteManager.deleteEmails(options, userContext);
expect(result.errors).toHaveLength(0);
});
xit('should retry operations after token refresh', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
// First call fails with expired token, second succeeds after refresh
mockAuthManager.getGmailClient
.mockRejectedValueOnce(new Error('Token expired'))
.mockResolvedValueOnce(mockGmailClient);
mockAuthManager.hasValidAuth
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true);
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
const result = await deleteManager.deleteEmails(options, userContext);
expect(result.errors).toHaveLength(0);
expect(mockAuthManager.refreshToken).toHaveBeenCalled();
});
it('should handle auth failures gracefully', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
seedTestData(dbManager, batchTestEmails.slice(0,2));
// Auth completely fails
mockAuthManager.hasValidAuth.mockResolvedValue(false);
mockAuthManager.getGmailClient.mockRejectedValue(new Error('Authentication failed'));
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext)).rejects.toThrow('Authentication failed');
});
});
describe('Token Refresh During Operations', () => {
// [TODO]handled from mcp client tests need to rethink if this needs to be handled in the delete logic
xit('should handle token refresh during large batch operations', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
await cleanupTestDatabase(dbManager, testDbDir);
const { dbManager: newDbManagerFactory, testDbDir: newTestDbDir } = await createTestDatabaseManager(uniqueUserId);
dbManager = await newDbManagerFactory.getUserDatabaseManager(uniqueUserId);
testDbDir = newTestDbDir;
await seedTestData(dbManager, batchTestEmails);
const userContext = createUserContext();
// Token expires during operation
let callCount = 0;
mockAuthManager.getGmailClient.mockImplementation(async () => {
callCount++;
if (callCount === 2) {
// Simulate token expiry on second batch
throw new Error('Token expired');
}
return mockGmailClient;
});
// Mock token refresh
mockAuthManager.refreshToken.mockResolvedValue(createMockOAuthToken());
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
const result = await deleteManager.deleteEmails(options, userContext);
expect(result.deleted).toBeGreaterThan(0);
expect(mockAuthManager.refreshToken).toHaveBeenCalled();
});
// should handle this test from mcp client test from claude
xit('should handle multiple users with different token states', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContexts = createMultiUserContexts();
// Mock different auth states for different users
mockAuthManager.hasValidAuth.mockImplementation(async (userId?: string) => {
return userId === userContexts[0].user_id; // Only first user has valid auth
});
mockAuthManager.getGmailClient.mockImplementation(async (userId?: string) => {
if (userId === userContexts[0].user_id) {
return mockGmailClient;
}
throw new Error('Token expired');
});
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
// First user should succeed
const result1 = await deleteManager.deleteEmails(options, userContexts[0]);
expect(result1.errors).toHaveLength(0);
// Second user should fail
await expect(deleteManager.deleteEmails(options, userContexts[1])).rejects.toThrow('Token expired');
});
});
describe('OAuth Scope Validation', () => {
it('should validate sufficient OAuth scopes for delete operations', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
// Mock token with insufficient scope
const limitedToken = {
...createMockOAuthToken(),
scope: 'https://www.googleapis.com/auth/gmail.readonly' // Read-only, no modify
};
seedTestData(dbManager, batchTestEmails.slice(0,2));
mockAuthManager.getStoredCredentials.mockResolvedValue(limitedToken);
mockAuthManager.getGmailClient.mockRejectedValue(oauthErrors.insufficientScope);
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext)).rejects.toThrow('Insufficient OAuth scope for delete operations');
});
it('should succeed with proper OAuth scopes', async () => {
const enhancedMockAuth = createEnhancedOAuthMockManager(mockGmailClient);
// Use existing mockAuthManager and enhance it
Object.assign(mockAuthManager, enhancedMockAuth);
const userContext = createUserContext();
// Mock token with sufficient scope
const fullToken = createMockOAuthToken();
mockAuthManager.getStoredCredentials.mockResolvedValue(fullToken);
mockAuthManager.getGmailClient.mockResolvedValue(mockGmailClient);
setupSuccessfulBatchModify(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
const result = await deleteManager.deleteEmails(options, userContext);
expect(result.errors).toHaveLength(0);
});
});
});
describe('OAuth Error Recovery and Edge Cases', () => {
let deleteManager: DeleteManager;
let mockGmailClient: any;
let mockAuthManager: any;
let dbManager: DatabaseManager;
let dbManagerFactory: any;
let testDbDir: string;
let defaultUserContext: { user_id: string; session_id: string };
let uniqueUserId: string;
const mockEmails: EmailIndex[] = [
{
id: 'email-low-1',
threadId: 'thread-1',
sender: 'sender1@example.com',
subject: 'Low Prio Email',
snippet: 'This is a low priority test email.',
size: 1024,
date: new Date('2023-01-01T10:00:00Z'),
category: 'low',
user_id: '', // will be set per test
archived: false,
promotional_score: 0.1,
spam_score: 0.1,
},
{
id: 'email-medium-1',
threadId: 'thread-2',
sender: 'sender2@example.com',
subject: 'Medium Prio Email',
snippet: 'This is a medium priority test email.',
size: 2048,
date: new Date('2023-01-02T11:00:00Z'),
category: 'medium',
user_id: '', // will be set per test
archived: false,
promotional_score: 0.2,
spam_score: 0.2,
},
];
beforeEach(async () => {
uniqueUserId = generateUniqueUserId();
const mocks = await createDeleteManagerWithRealDb(undefined, uniqueUserId);
deleteManager = mocks.deleteManager;
mockGmailClient = mocks.mockGmailClient;
mockAuthManager = mocks.mockAuthManager;
dbManagerFactory = mocks.dbManagerFactory;
testDbDir = mocks.testDbDir;
defaultUserContext = createUserContext(uniqueUserId);
dbManager = await dbManagerFactory.getUserDatabaseManager(uniqueUserId);
// Set user_id on all mock emails for this test
const emails = mockEmails.map(e => ({ ...e, user_id: uniqueUserId }));
await seedTestData(dbManager, emails, uniqueUserId);
// Setup console capture for error logging tests
console.error = jest.fn();
});
afterEach(async () => {
jest.restoreAllMocks();
await cleanupTestDatabase(dbManager, testDbDir);
});
describe('Network Failures', () => {
it('should handle network failures during OAuth', async () => {
// Setup network error
mockAuthManager._setNetworkError(true);
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext))
.rejects.toThrow('Network error');
});
// [TODO] needs be handled from within the delete logic not currently implemented
xit('should retry on network failures', async () => {
// First call fails, second succeeds
mockAuthManager.getGmailClient
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
// Mock successful batch modify
mockGmailClient.users.messages.batchModify.mockResolvedValue({});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(mockAuthManager.getGmailClient).toHaveBeenCalledTimes(2);
expect(result.deleted).toBeGreaterThanOrEqual(0);
});
});
describe('Token Expiration', () => {
it('should handle expired tokens', async () => {
mockAuthManager._setTokenExpired(true);
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext))
.rejects.toThrow('Token expired');
});
// [TODO] needs to be handled from within the delete logic not currently implemented
xit('should refresh expired tokens', async () => {
// First call fails with expired token, second succeeds after refresh
mockAuthManager.getGmailClient
.mockRejectedValueOnce(new Error('Token expired'))
.mockResolvedValueOnce(mockGmailClient);
const options = createDeleteOptions({ category: 'low' });
// Mock successful batch modify
mockGmailClient.users.messages.batchModify.mockResolvedValue({});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(mockAuthManager.refreshAccessToken).toHaveBeenCalled();
expect(result.deleted).toBeGreaterThanOrEqual(0);
});
});
describe('Rate Limiting', () => {
it('should handle rate limiting', async () => {
mockAuthManager._setRateLimited(true);
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext))
.rejects.toThrow('Rate limit exceeded');
});
// [TODO] backoff feature is not yet implemented in the delete logic to be handled in the future
xit('should implement backoff for rate limits', async () => {
// First call fails with rate limit, second succeeds
const rateLimitError = new Error('Rate limit exceeded');
(rateLimitError as any).code = 429;
mockAuthManager.getGmailClient
.mockRejectedValueOnce(rateLimitError)
.mockResolvedValueOnce(mockGmailClient);
const mockBackoff = jest.spyOn(global, 'setTimeout');
const options = createDeleteOptions({ category: 'low' });
// Mock successful batch modify
mockGmailClient.users.messages.batchModify.mockResolvedValue({});
const result = await deleteManager.deleteEmails(options, defaultUserContext);
expect(mockBackoff).toHaveBeenCalled();
expect(result.deleted).toBeGreaterThanOrEqual(0);
});
});
xdescribe('Token Revocation', () => {
it('should handle revoked tokens', async () => {
mockAuthManager._setRevoked(true);
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext))
.rejects.toThrow('Token has been revoked');
});
});
xdescribe('Scope Validation', () => {
it('should validate required scopes', async () => {
mockAuthManager._setScopes(['https://www.googleapis.com/auth/gmail.readonly']);
const options = createDeleteOptions({ category: 'low' });
await expect(deleteManager.deleteEmails(options, defaultUserContext))
.rejects.toThrow('Insufficient scopes');
});
});
describe('Concurrent Operations', () => {
it('should handle multiple concurrent operations', async () => {
const numConcurrent = 3;
const promises: Promise<any>[] = [];
// Set up mock to alternate between success and failure
for (let i = 0; i < numConcurrent; i++) {
if (i % 2 === 0) {
mockAuthManager.getGmailClient.mockResolvedValueOnce(mockGmailClient);
} else {
mockAuthManager.getGmailClient.mockRejectedValueOnce(new Error('Network error'));
}
const options = createDeleteOptions({
category: i % 2 === 0 ? 'low' : 'medium'
});
// Mock successful batch modify for successful operations
if (i % 2 === 0) {
mockGmailClient.users.messages.batchModify.mockResolvedValueOnce({});
}
promises.push(deleteManager.deleteEmails(options, defaultUserContext));
}
const results = await Promise.allSettled(promises);
const successes = results.filter(r => r.status === 'fulfilled').length;
const failures = results.filter(r => r.status === 'rejected').length;
expect(successes).toBeGreaterThan(0);
expect(failures).toBeGreaterThan(0);
expect(successes + failures).toBe(numConcurrent);
});
});
});
});