From a5975a9b568c48dbe970a86ae112cd03564c8aee Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Mon, 15 Sep 2025 15:46:05 +0200 Subject: [PATCH] Fix CI/CD problems --- src/app/__tests__/page.test.tsx | 38 ++++++++++++ src/server/lib/git.ts | 7 ++- src/server/lib/scripts.ts | 62 +++++++++++--------- src/server/services/githubJsonService.ts | 73 ++++++++++++++---------- src/server/services/scriptDownloader.ts | 58 +++++++++++-------- 5 files changed, 157 insertions(+), 81 deletions(-) diff --git a/src/app/__tests__/page.test.tsx b/src/app/__tests__/page.test.tsx index 7ffef68..c83779a 100644 --- a/src/app/__tests__/page.test.tsx +++ b/src/app/__tests__/page.test.tsx @@ -2,6 +2,44 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import Home from '../page' +// Mock tRPC +vi.mock('~/trpc/react', () => ({ + api: { + scripts: { + getRepoStatus: { + useQuery: vi.fn(() => ({ + data: { isRepo: true, isBehind: false, branch: 'main', lastCommit: 'abc123' }, + refetch: vi.fn(), + })), + }, + getScriptCards: { + useQuery: vi.fn(() => ({ + data: { success: true, cards: [] }, + isLoading: false, + error: null, + })), + }, + getCtScripts: { + useQuery: vi.fn(() => ({ + data: { scripts: [] }, + isLoading: false, + error: null, + })), + }, + getScriptBySlug: { + useQuery: vi.fn(() => ({ + data: null, + })), + }, + fullUpdateRepo: { + useMutation: vi.fn(() => ({ + mutate: vi.fn(), + })), + }, + }, + }, +})) + // Mock child components vi.mock('../_components/ScriptsGrid', () => ({ ScriptsGrid: ({ onInstallScript }: { onInstallScript?: (path: string, name: string) => void }) => ( diff --git a/src/server/lib/git.ts b/src/server/lib/git.ts index 1a71837..ed7acca 100644 --- a/src/server/lib/git.ts +++ b/src/server/lib/git.ts @@ -9,14 +9,17 @@ const execAsync = promisify(exec); export class GitManager { private git: SimpleGit; private repoPath: string; - private scriptsDir: string; + private scriptsDir: string | null = null; constructor() { this.repoPath = process.cwd(); - this.scriptsDir = join(this.repoPath, env.SCRIPTS_DIRECTORY); this.git = simpleGit(this.repoPath); } + private initializeConfig() { + this.scriptsDir ??= join(this.repoPath, env.SCRIPTS_DIRECTORY); + } + /** * Check if the repository is behind the remote */ diff --git a/src/server/lib/scripts.ts b/src/server/lib/scripts.ts index 1ff918c..20a8bb4 100644 --- a/src/server/lib/scripts.ts +++ b/src/server/lib/scripts.ts @@ -16,38 +16,45 @@ export interface ScriptInfo { } export class ScriptManager { - private scriptsDir: string; - private allowedExtensions: string[]; - private allowedPaths: string[]; - private maxExecutionTime: number; + private scriptsDir: string | null = null; + private allowedExtensions: string[] | null = null; + private allowedPaths: string[] | null = null; + private maxExecutionTime: number | null = null; constructor() { - // Handle both absolute and relative paths for testing - this.scriptsDir = env.SCRIPTS_DIRECTORY.startsWith('/') - ? env.SCRIPTS_DIRECTORY - : join(process.cwd(), env.SCRIPTS_DIRECTORY); - this.allowedExtensions = env.ALLOWED_SCRIPT_EXTENSIONS.split(',').map(ext => ext.trim()); - this.allowedPaths = env.ALLOWED_SCRIPT_PATHS.split(',').map(path => path.trim()); - this.maxExecutionTime = parseInt(env.MAX_SCRIPT_EXECUTION_TIME, 10); + // Initialize lazily to avoid accessing env vars during module load + } + + private initializeConfig() { + if (this.scriptsDir === null) { + // Handle both absolute and relative paths for testing + this.scriptsDir = env.SCRIPTS_DIRECTORY.startsWith('/') + ? env.SCRIPTS_DIRECTORY + : join(process.cwd(), env.SCRIPTS_DIRECTORY); + this.allowedExtensions = env.ALLOWED_SCRIPT_EXTENSIONS.split(',').map(ext => ext.trim()); + this.allowedPaths = env.ALLOWED_SCRIPT_PATHS.split(',').map(path => path.trim()); + this.maxExecutionTime = parseInt(env.MAX_SCRIPT_EXECUTION_TIME, 10); + } } /** * Get all available scripts in the scripts directory */ async getScripts(): Promise { + this.initializeConfig(); try { - const files = await readdir(this.scriptsDir); + const files = await readdir(this.scriptsDir!); const scripts: ScriptInfo[] = []; for (const file of files) { - const filePath = join(this.scriptsDir, file); + const filePath = join(this.scriptsDir!, file); const stats = await stat(filePath); if (stats.isFile()) { const extension = extname(file); // Check if file extension is allowed - if (this.allowedExtensions.includes(extension)) { + if (this.allowedExtensions!.includes(extension)) { // Check if file is executable const executable = await this.isExecutable(filePath); @@ -74,8 +81,9 @@ export class ScriptManager { * Get all available scripts in the ct subdirectory */ async getCtScripts(): Promise { + this.initializeConfig(); try { - const ctDir = join(this.scriptsDir, 'ct'); + const ctDir = join(this.scriptsDir!, 'ct'); const files = await readdir(ctDir); const scripts: ScriptInfo[] = []; @@ -87,7 +95,7 @@ export class ScriptManager { const extension = extname(file); // Check if file extension is allowed - if (this.allowedExtensions.includes(extension)) { + if (this.allowedExtensions!.includes(extension)) { // Check if file is executable const executable = await this.isExecutable(filePath); @@ -143,8 +151,9 @@ export class ScriptManager { * Validate if a script path is allowed to be executed */ validateScriptPath(scriptPath: string): { valid: boolean; message?: string } { + this.initializeConfig(); const resolvedPath = resolve(scriptPath); - const scriptsDirResolved = resolve(this.scriptsDir); + const scriptsDirResolved = resolve(this.scriptsDir!); // Check if the script is within the allowed directory if (!resolvedPath.startsWith(scriptsDirResolved)) { @@ -158,7 +167,7 @@ export class ScriptManager { const relativePath = resolvedPath.replace(scriptsDirResolved, '').replace(/\\/g, '/'); const normalizedRelativePath = relativePath.startsWith('/') ? relativePath : '/' + relativePath; - const isAllowed = this.allowedPaths.some(allowedPath => { + const isAllowed = this.allowedPaths!.some(allowedPath => { const normalizedAllowedPath = allowedPath.startsWith('/') ? allowedPath : '/' + allowedPath; // For root path '/', allow files directly in the scripts directory (no subdirectories) if (normalizedAllowedPath === '/') { @@ -177,10 +186,10 @@ export class ScriptManager { // Check file extension const extension = extname(scriptPath); - if (!this.allowedExtensions.includes(extension)) { + if (!this.allowedExtensions!.includes(extension)) { return { valid: false, - message: `File extension '${extension}' is not allowed. Allowed extensions: ${this.allowedExtensions.join(', ')}` + message: `File extension '${extension}' is not allowed. Allowed extensions: ${this.allowedExtensions!.join(', ')}` }; } @@ -227,7 +236,7 @@ export class ScriptManager { // Spawn the process const childProcess = spawn(command, args, { - cwd: this.scriptsDir, + cwd: this.scriptsDir!, stdio: ['pipe', 'pipe', 'pipe'], shell: true }); @@ -237,7 +246,7 @@ export class ScriptManager { if (!childProcess.killed) { childProcess.kill('SIGTERM'); } - }, this.maxExecutionTime); + }, this.maxExecutionTime!); // Clean up timeout when process exits childProcess.on('exit', () => { @@ -268,11 +277,12 @@ export class ScriptManager { allowedPaths: string[]; maxExecutionTime: number; } { + this.initializeConfig(); return { - path: this.scriptsDir, - allowedExtensions: this.allowedExtensions, - allowedPaths: this.allowedPaths, - maxExecutionTime: this.maxExecutionTime + path: this.scriptsDir!, + allowedExtensions: this.allowedExtensions!, + allowedPaths: this.allowedPaths!, + maxExecutionTime: this.maxExecutionTime! }; } } diff --git a/src/server/services/githubJsonService.ts b/src/server/services/githubJsonService.ts index b3499c9..679cdf8 100644 --- a/src/server/services/githubJsonService.ts +++ b/src/server/services/githubJsonService.ts @@ -4,37 +4,44 @@ import { env } from '~/env.js'; import type { Script, ScriptCard, GitHubFile } from '~/types/script'; export class GitHubJsonService { - private baseUrl: string; - private repoUrl: string; - private branch: string; - private jsonFolder: string; - private localJsonDirectory: string; + private baseUrl: string | null = null; + private repoUrl: string | null = null; + private branch: string | null = null; + private jsonFolder: string | null = null; + private localJsonDirectory: string | null = null; private scriptCache: Map = new Map(); constructor() { - this.repoUrl = env.REPO_URL ?? ""; - this.branch = env.REPO_BRANCH; - this.jsonFolder = env.JSON_FOLDER; - this.localJsonDirectory = join(process.cwd(), 'scripts', 'json'); - - // Only validate GitHub URL if it's provided - if (this.repoUrl) { - // Extract owner and repo from the URL - const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl); - if (!urlMatch) { - throw new Error(`Invalid GitHub repository URL: ${this.repoUrl}`); - } + // Initialize lazily to avoid accessing env vars during module load + } + + private initializeConfig() { + if (this.repoUrl === null) { + this.repoUrl = env.REPO_URL ?? ""; + this.branch = env.REPO_BRANCH; + this.jsonFolder = env.JSON_FOLDER; + this.localJsonDirectory = join(process.cwd(), 'scripts', 'json'); - const [, owner, repo] = urlMatch; - this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`; - } else { - // Set a dummy base URL if no REPO_URL is provided - this.baseUrl = ""; + // Only validate GitHub URL if it's provided + if (this.repoUrl) { + // Extract owner and repo from the URL + const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl); + if (!urlMatch) { + throw new Error(`Invalid GitHub repository URL: ${this.repoUrl}`); + } + + const [, owner, repo] = urlMatch; + this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`; + } else { + // Set a dummy base URL if no REPO_URL is provided + this.baseUrl = ""; + } } } private async fetchFromGitHub(endpoint: string): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { + this.initializeConfig(); + const response = await fetch(`${this.baseUrl!}${endpoint}`, { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'PVEScripts-Local/1.0', @@ -49,7 +56,8 @@ export class GitHubJsonService { } private async downloadJsonFile(filePath: string): Promise