Path: blob/main/extensions/git/src/test/askpassManager.test.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import 'mocha';6import * as assert from 'assert';7import * as fs from 'fs';8import * as path from 'path';9import * as os from 'os';10import { ensureAskpassScripts } from '../askpassManager';11import { Event, EventEmitter, LogLevel, LogOutputChannel } from 'vscode';1213class MockLogOutputChannel implements LogOutputChannel {14logLevel: LogLevel = LogLevel.Info;15onDidChangeLogLevel: Event<LogLevel> = new EventEmitter<LogLevel>().event;16private logs: { level: string; message: string }[] = [];1718trace(message: string, ..._args: any[]): void {19this.logs.push({ level: 'trace', message });20}21debug(message: string, ..._args: any[]): void {22this.logs.push({ level: 'debug', message });23}24info(message: string, ..._args: any[]): void {25this.logs.push({ level: 'info', message });26}27warn(message: string, ..._args: any[]): void {28this.logs.push({ level: 'warn', message });29}30error(error: string | Error, ..._args: any[]): void {31this.logs.push({ level: 'error', message: error.toString() });32}3334name: string = 'MockLogOutputChannel';35append(_value: string): void { }36appendLine(_value: string): void { }37replace(_value: string): void { }38clear(): void { }39show(_column?: unknown, _preserveFocus?: unknown): void { }40hide(): void { }41dispose(): void { }4243getLogs(): { level: string; message: string }[] {44return this.logs;45}4647hasLog(level: string, messageSubstring: string): boolean {48return this.logs.some(log => log.level === level && log.message.includes(messageSubstring));49}50}5152// Helper to set mtime on a directory53async function setDirectoryMtime(dirPath: string, mtime: Date): Promise<void> {54await fs.promises.utimes(dirPath, mtime, mtime);55}5657suite('askpassManager', () => {58let tempDir: string;59let sourceDir: string;6061setup(async () => {62// Create a temporary directory for testing63tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'askpass-test-'));6465// Create source directory with dummy askpass files66sourceDir = path.join(tempDir, 'source');67await fs.promises.mkdir(sourceDir, { recursive: true });6869const askpassFiles = ['askpass.sh', 'askpass-main.js', 'ssh-askpass.sh', 'askpass-empty.sh', 'ssh-askpass-empty.sh'];70for (const file of askpassFiles) {71await fs.promises.writeFile(path.join(sourceDir, file), `#!/bin/sh\n# ${file}\n`);72}73});7475teardown(async () => {76// Clean up temporary directory77try {78await fs.promises.rm(tempDir, { recursive: true, force: true });79} catch {80// Ignore errors during cleanup81}82});8384test('garbage collection removes old directories', async function () {85const storageDir = path.join(tempDir, 'storage');86const askpassBaseDir = path.join(storageDir, 'askpass');87const logger = new MockLogOutputChannel();8889// Create old directories with old mtimes (8 days ago)90const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000));91const oldDirs = ['oldhash1', 'oldhash2'];9293for (const dirName of oldDirs) {94const dirPath = path.join(askpassBaseDir, dirName);95await fs.promises.mkdir(dirPath, { recursive: true });96await fs.promises.writeFile(path.join(dirPath, 'test.txt'), 'old');97await setDirectoryMtime(dirPath, oldDate);98}99100// Create a recent directory (1 day ago)101const recentDate = new Date(Date.now() - (1 * 24 * 60 * 60 * 1000));102const recentDir = path.join(askpassBaseDir, 'recenthash');103await fs.promises.mkdir(recentDir, { recursive: true });104await fs.promises.writeFile(path.join(recentDir, 'test.txt'), 'recent');105await setDirectoryMtime(recentDir, recentDate);106107// Call ensureAskpassScripts which should trigger garbage collection when creating a new directory108await ensureAskpassScripts(sourceDir, storageDir, logger);109110// Check that old directories were removed111for (const dirName of oldDirs) {112const dirPath = path.join(askpassBaseDir, dirName);113const exists = await fs.promises.access(dirPath).then(() => true).catch(() => false);114assert.strictEqual(exists, false, `Old directory ${dirName} should have been removed`);115}116117// Check that recent directory still exists118const recentExists = await fs.promises.access(recentDir).then(() => true).catch(() => false);119assert.strictEqual(recentExists, true, 'Recent directory should still exist');120121// Check logs122assert.ok(logger.hasLog('info', 'Removing old askpass directory'), 'Should log removal of old directories');123});124125test('garbage collection skips non-directory entries', async function () {126const storageDir = path.join(tempDir, 'storage');127const askpassBaseDir = path.join(storageDir, 'askpass');128const logger = new MockLogOutputChannel();129130// Create a file in the askpass directory (not a directory)131await fs.promises.mkdir(askpassBaseDir, { recursive: true });132const filePath = path.join(askpassBaseDir, 'somefile.txt');133await fs.promises.writeFile(filePath, 'test');134135// Set old mtime136const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000));137await fs.promises.utimes(filePath, oldDate, oldDate);138139// Call ensureAskpassScripts which should trigger garbage collection140await ensureAskpassScripts(sourceDir, storageDir, logger);141142// Check that file still exists (should not be removed)143const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);144assert.strictEqual(exists, true, 'Non-directory file should not be removed');145});146147test('mtime is updated on existing directory', async function () {148const storageDir = path.join(tempDir, 'storage');149const logger = new MockLogOutputChannel();150151// Call ensureAskpassScripts to create the directory152const paths1 = await ensureAskpassScripts(sourceDir, storageDir, logger);153154// Get the directory path and its initial mtime155const askpassDir = path.dirname(paths1.askpass);156const stat1 = await fs.promises.stat(askpassDir);157const mtime1 = stat1.mtime.getTime();158159// Wait a bit to ensure time difference160await new Promise(resolve => setTimeout(resolve, 100));161162// Call again (should update mtime)163await ensureAskpassScripts(sourceDir, storageDir, logger);164165// Check that mtime was updated166const stat2 = await fs.promises.stat(askpassDir);167const mtime2 = stat2.mtime.getTime();168169assert.ok(mtime2 > mtime1, 'Mtime should be updated on subsequent calls');170});171172test('garbage collection handles empty askpass directory', async function () {173const storageDir = path.join(tempDir, 'storage');174const logger = new MockLogOutputChannel();175176// Don't create any askpass directories, just call ensureAskpassScripts177await ensureAskpassScripts(sourceDir, storageDir, logger);178179// Should complete without errors180assert.ok(true, 'Should handle empty or non-existent askpass directory gracefully');181});182183test('current content-addressed directory is not removed', async function () {184const storageDir = path.join(tempDir, 'storage');185const logger = new MockLogOutputChannel();186187// Create the current content-addressed directory188const paths = await ensureAskpassScripts(sourceDir, storageDir, logger);189const currentDir = path.dirname(paths.askpass);190191// Set its mtime to 8 days ago (would normally be removed)192const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000));193await setDirectoryMtime(currentDir, oldDate);194195// Call again which should trigger GC196await ensureAskpassScripts(sourceDir, storageDir, logger);197198// Current directory should still exist199const exists = await fs.promises.access(currentDir).then(() => true).catch(() => false);200assert.strictEqual(exists, true, 'Current content-addressed directory should not be removed');201});202});203204205