diff --git a/.gitignore b/.gitignore index b1b4744..0d625e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ package-lock.json yarn.lock pnpm-lock.yaml logs +test-convert-logs tmp/ testing/ .claude/settings.local.json diff --git a/src/logger.ts b/src/logger.ts index 151e0ae..81078c2 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -38,6 +38,10 @@ function appendToLogFile(filePath: string, entry: unknown) { try { const logPath = getLogPath(); confirmOrCreateFolder(logPath); + + // Sanitize file name for Windows compatibility + filePath = filePath.replace(/:/g, '-'); + const fullPath = `${logPath}/${filePath}`; // Use synchronous append to ensure immediate write diff --git a/src/migrate/cli.ts b/src/migrate/cli.ts index 4d6b237..de66397 100644 --- a/src/migrate/cli.ts +++ b/src/migrate/cli.ts @@ -1285,7 +1285,8 @@ export async function runCLI(cliArgs?: CLIArgs) { // Use CLI args as initial values if provided const initialTransformer = cliArgs?.transformer || savedSettings.key || transformers[0].key; - const initialFile = cliArgs?.file || savedSettings.file || 'users.json'; + const initialFile = + cliArgs?.file || savedSettings.file || 'samples/clerk.csv'; const initialResumeAfter = cliArgs?.resumeAfter || ''; const initialArgs = await p.group( diff --git a/tests/convert-logs.test.ts b/tests/convert-logs.test.ts index b4985fc..28e9b74 100644 --- a/tests/convert-logs.test.ts +++ b/tests/convert-logs.test.ts @@ -8,7 +8,8 @@ import { } from 'node:fs'; import path from 'node:path'; -const LOGS_DIR = path.join(process.cwd(), 'logs'); +// Use a unique directory to avoid conflicts with logger.test.ts which also uses 'logs/' +const LOGS_DIR = path.join(process.cwd(), 'test-convert-logs'); // Helper to clean up logs directory const cleanupLogs = () => { diff --git a/tests/logger.test.ts b/tests/logger.test.ts index 7e3bb2f..e775596 100644 --- a/tests/logger.test.ts +++ b/tests/logger.test.ts @@ -7,13 +7,29 @@ import { importLogger, validationLogger, } from '../src/logger'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs'; -// Helper to clean up logs directory -const cleanupLogs = () => { - closeAllStreams(); // Close streams before cleanup +// Snapshot of files in logs/ before each test so we only clean up test-created files +let existingLogFiles: Set = new Set(); + +// Capture which files already exist before a test runs +const snapshotExistingLogs = () => { + closeAllStreams(); if (existsSync('logs')) { - rmSync('logs', { recursive: true }); + existingLogFiles = new Set(readdirSync('logs')); + } else { + existingLogFiles = new Set(); + } +}; + +// Only remove files created by the test, never delete the logs/ directory +const cleanupTestLogs = () => { + closeAllStreams(); + if (!existsSync('logs')) return; + for (const file of readdirSync('logs')) { + if (!existingLogFiles.has(file)) { + unlinkSync(`logs/${file}`); + } } }; @@ -28,8 +44,8 @@ const readNDJSON = (filePath: string): Record[] => { }; describe('errorLogger', () => { - beforeEach(cleanupLogs); - afterEach(cleanupLogs); + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); test('logs a single error to migration log', () => { const dateTime = 'error-single-test'; @@ -169,8 +185,8 @@ describe('errorLogger', () => { }); describe('validationLogger', () => { - beforeEach(cleanupLogs); - afterEach(cleanupLogs); + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); test('logs a validation error to migration log', () => { const dateTime = 'validation-basic-test'; @@ -279,8 +295,8 @@ describe('validationLogger', () => { }); describe('importLogger', () => { - beforeEach(cleanupLogs); - afterEach(cleanupLogs); + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); test('logs a successful import', () => { const dateTime = 'import-success-test'; @@ -351,8 +367,8 @@ describe('importLogger', () => { }); describe('deleteErrorLogger', () => { - beforeEach(cleanupLogs); - afterEach(cleanupLogs); + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); test('logs a single error to user deletion log', () => { const dateTime = 'delete-error-single-test'; @@ -468,8 +484,8 @@ describe('deleteErrorLogger', () => { }); describe('deleteLogger', () => { - beforeEach(cleanupLogs); - afterEach(cleanupLogs); + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); test('logs a successful deletion', () => { const dateTime = 'delete-success-test'; @@ -524,8 +540,8 @@ describe('deleteLogger', () => { }); describe('mixed logging', () => { - beforeEach(cleanupLogs); - afterEach(cleanupLogs); + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); test('error and validation logs go to same migration log file', () => { const dateTime = 'mixed-errors-test'; @@ -593,3 +609,27 @@ describe('mixed logging', () => { expect(migrationLog[2].clerkUserId).toBe('clerk_2'); }); }); + +describe('filename sanitization', () => { + beforeEach(snapshotExistingLogs); + afterEach(cleanupTestLogs); + + test('replaces colons with hyphens in log filenames for Windows compatibility', () => { + const dateTime = '2026-01-20T14:30:45'; + + importLogger( + { userId: 'user_1', status: 'success', clerkUserId: 'clerk_1' }, + dateTime + ); + + const sanitizedFileName = 'migration-2026-01-20T14-30-45.log'; + const log = readNDJSON(`logs/${sanitizedFileName}`); + expect(log).toHaveLength(1); + expect(log[0].userId).toBe('user_1'); + + // Verify no file with colons was created + const logFiles = readdirSync('logs'); + expect(logFiles).not.toContain('migration-2026-01-20T14:30:45.log'); + expect(logFiles).toContain(sanitizedFileName); + }); +}); diff --git a/tests/migrate/functions.test.ts b/tests/migrate/functions.test.ts index 8be83b4..388adec 100644 --- a/tests/migrate/functions.test.ts +++ b/tests/migrate/functions.test.ts @@ -1,8 +1,32 @@ -import { describe, expect, test } from 'vitest'; +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { existsSync, readdirSync, unlinkSync } from 'node:fs'; import { loadUsersFromFile } from '../../src/migrate/functions'; import { transformKeys } from '../../src/utils'; import { transformers } from '../../src/transformers'; +// Snapshot of files in logs/ before each test so we only clean up test-created files +let existingLogFiles: Set = new Set(); + +const snapshotExistingLogs = () => { + if (existsSync('logs')) { + existingLogFiles = new Set(readdirSync('logs')); + } else { + existingLogFiles = new Set(); + } +}; + +const cleanupTestLogs = () => { + if (!existsSync('logs')) return; + for (const file of readdirSync('logs')) { + if (!existingLogFiles.has(file)) { + unlinkSync(`logs/${file}`); + } + } +}; + +beforeEach(snapshotExistingLogs); +afterEach(cleanupTestLogs); + test('Clerk - loadUsersFromFile - JSON', async () => { const { users: usersFromClerk } = await loadUsersFromFile( './samples/clerk.json', diff --git a/tests/migrate/import-users.test.ts b/tests/migrate/import-users.test.ts index bd1d3a7..28f3e9c 100644 --- a/tests/migrate/import-users.test.ts +++ b/tests/migrate/import-users.test.ts @@ -1,6 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { existsSync, rmSync } from 'node:fs'; - +import { beforeEach, describe, expect, test, vi } from 'vitest'; // Mock @clerk/backend before importing the module const mockCreateUser = vi.fn(); const mockCreateEmailAddress = vi.fn(); @@ -100,21 +98,9 @@ import { } from '../../src/migrate/import-users'; import * as logger from '../../src/logger'; -// Helper to clean up logs directory -const cleanupLogs = () => { - if (existsSync('logs')) { - rmSync('logs', { recursive: true, force: true, maxRetries: 3 }); - } -}; - describe('importUsers', () => { beforeEach(() => { vi.clearAllMocks(); - cleanupLogs(); - }); - - afterEach(() => { - cleanupLogs(); }); describe('createUser API calls', () => { @@ -405,11 +391,6 @@ describe('importUsers edge cases', () => { beforeEach(() => { vi.clearAllMocks(); mockCreatePhoneNumber.mockReset(); - cleanupLogs(); - }); - - afterEach(() => { - cleanupLogs(); }); test('handles empty user array', async () => {