Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package-lock.json
yarn.lock
pnpm-lock.yaml
logs
test-convert-logs
tmp/
testing/
.claude/settings.local.json
Expand Down
4 changes: 4 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/migrate/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion tests/convert-logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
74 changes: 57 additions & 17 deletions tests/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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}`);
}
}
};

Expand All @@ -28,8 +44,8 @@ const readNDJSON = (filePath: string): Record<string, unknown>[] => {
};

describe('errorLogger', () => {
beforeEach(cleanupLogs);
afterEach(cleanupLogs);
beforeEach(snapshotExistingLogs);
afterEach(cleanupTestLogs);

test('logs a single error to migration log', () => {
const dateTime = 'error-single-test';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
26 changes: 25 additions & 1 deletion tests/migrate/functions.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> = 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',
Expand Down
21 changes: 1 addition & 20 deletions tests/migrate/import-users.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -405,11 +391,6 @@ describe('importUsers edge cases', () => {
beforeEach(() => {
vi.clearAllMocks();
mockCreatePhoneNumber.mockReset();
cleanupLogs();
});

afterEach(() => {
cleanupLogs();
});

test('handles empty user array', async () => {
Expand Down