diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9436e09b..e4f8f7e60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ permissions: pull-requests: write jobs: - build: + # Ubuntu build with MongoDB matrix (9 combinations: 3 Node × 3 MongoDB) + build-ubuntu: runs-on: ubuntu-latest strategy: @@ -38,7 +39,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Start MongoDB - uses: supercharge/mongodb-github-action@315db7fe45ac2880b7758f1933e6e5d59afd5e94 # ratchet:supercharge/mongodb-github-action@1.12.1 + uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 with: mongodb-version: ${{ matrix.mongodb-version }} @@ -65,24 +66,21 @@ jobs: with: files: ./coverage/lcov.info token: ${{ secrets.CODECOV_TOKEN }} - # - name: Exit if coverage condition not met - # if: ${{ steps.test.outputs.exit_code }} != 0 - # run: exit ${{ steps.test.outputs.exit_code }} - name: Build frontend run: npm run build-ui - name: Save build folder - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # ratchet:actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: build-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} + name: build-ubuntu-node-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} if-no-files-found: error path: build - name: Download the build folders - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # ratchet:actions/download-artifact@v5 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 with: - name: build-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} + name: build-ubuntu-node-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} path: build - name: Run cypress test @@ -93,37 +91,80 @@ jobs: wait-on-timeout: 120 command: npm run cypress:run + # Windows build - single combination for development support + build-windows: + runs-on: windows-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Use Node.js 24.x + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: 24.x + + - name: Enable Windows Developer Mode + shell: powershell + run: | + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + + - name: Install dependencies + run: npm ci + + - name: Check Types (Server) + run: npm run check-types:server + + - name: Build TypeScript + run: npm run build-ts + + - name: Test + id: test + shell: bash + run: | + npm run test-coverage-ci + npm run test-coverage-ci --workspaces --if-present + + - name: Build frontend + run: npm run build-ui + # Execute a final job to collect the results and report a single check status results: if: ${{ always() }} runs-on: ubuntu-latest name: build result - needs: [build] + needs: [build-ubuntu, build-windows] steps: - name: Check build results run: | - result="${{ needs.build.result }}" - if [[ $result == "success" || $result == "skipped" ]]; then + ubuntu_result="${{ needs.build-ubuntu.result }}" + windows_result="${{ needs.build-windows.result }}" + if [[ ($ubuntu_result == "success" || $ubuntu_result == "skipped") && ($windows_result == "success" || $windows_result == "skipped") ]]; then echo "### ✅ All builds passed" >> $GITHUB_STEP_SUMMARY exit 0 else echo "### ❌ Some builds failed" >> $GITHUB_STEP_SUMMARY + echo "- Ubuntu: $ubuntu_result" >> $GITHUB_STEP_SUMMARY + echo "- Windows: $windows_result" >> $GITHUB_STEP_SUMMARY exit 1 fi - name: Parse failed matrix jobs - if: needs.build.result == 'failure' + if: needs.build-ubuntu.result == 'failure' || needs.build-windows.result == 'failure' run: | echo "## Failed Matrix Combinations" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "| Node Version | MongoDB Version | Status |" >> $GITHUB_STEP_SUMMARY - echo "|--------------|-----------------|--------|" >> $GITHUB_STEP_SUMMARY - - # Parse the matrix results from the build job - results='${{ toJSON(needs.build.outputs) }}' + echo "| OS | Node Version | MongoDB Version | Status |" >> $GITHUB_STEP_SUMMARY + echo "|----|--------------|-----------------|--------|" >> $GITHUB_STEP_SUMMARY # Since we can't directly get individual matrix job statuses, # we'll note that the build job failed - echo "| Multiple | Multiple | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + echo "| Multiple | Multiple | Multiple | ❌ Failed |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "⚠️ Check the [build job logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details on which specific matrix combinations failed." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/unused-dependencies.yml b/.github/workflows/unused-dependencies.yml index b3e133fa1..d2a9b6979 100644 --- a/.github/workflows/unused-dependencies.yml +++ b/.github/workflows/unused-dependencies.yml @@ -21,7 +21,7 @@ jobs: node-version: '22.x' - name: 'Run depcheck' run: | - npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths,quicktype,history,@types/domutils,@vitest/coverage-v8" + npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths,quicktype,history,@types/domutils,@vitest/coverage-v8,cross-env" echo $? if [[ $? == 1 ]]; then echo "Unused dependencies or devDependencies found" diff --git a/package-lock.json b/package-lock.json index 1febd2a99..6e34184ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,7 @@ "@types/yargs": "^17.0.35", "@vitejs/plugin-react": "^5.1.2", "@vitest/coverage-v8": "^3.2.4", + "cross-env": "^10.1.0", "cypress": "^15.9.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", @@ -1935,6 +1936,13 @@ "version": "0.8.0", "license": "MIT" }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -3070,8 +3078,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.1", - "license": "MIT", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", + "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" @@ -6535,6 +6544,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-fetch": { "version": "4.1.0", "dev": true, @@ -8015,14 +8042,15 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -8808,24 +8836,13 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "engines": { "node": ">= 12" } }, - "node_modules/ip-address/node_modules/jsbn": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "license": "BSD-3-Clause" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -12841,10 +12858,11 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "license": "MIT", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -13666,8 +13684,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsscmp": { "version": "1.0.6", diff --git a/package.json b/package.json index dc442b753..d8c9fb829 100644 --- a/package.json +++ b/package.json @@ -49,16 +49,16 @@ "server": "ALLOWED_ORIGINS=* tsx index.ts", "start": "concurrently \"npm run server\" \"npm run client\"", "build": "npm run generate-config-types && npm run build-ui && npm run build-ts", - "build-ts": "tsc --project tsconfig.publish.json && ./scripts/fix-shebang.sh", + "build-ts": "tsc --project tsconfig.publish.json && node scripts/fix-shebang.js", "build-ui": "vite build", "check-types": "tsc", "check-types:server": "tsc --project tsconfig.publish.json --noEmit", - "test": "NODE_ENV=test vitest --run --dir ./test", + "test": "cross-env NODE_ENV=test vitest --run --dir ./test", "test:e2e": "vitest run --config vitest.config.e2e.ts", "test:e2e:watch": "vitest --config vitest.config.e2e.ts", - "test-coverage": "NODE_ENV=test vitest --run --dir ./test --coverage", - "test-coverage-ci": "NODE_ENV=test vitest --run --dir ./test --coverage.enabled=true --coverage.reporter=lcovonly --coverage.reporter=text", - "test-watch": "NODE_ENV=test vitest --dir ./test --watch", + "test-coverage": "cross-env NODE_ENV=test vitest --run --dir ./test --coverage", + "test-coverage-ci": "cross-env NODE_ENV=test vitest --run --dir ./test --coverage.enabled=true --coverage.reporter=lcovonly --coverage.reporter=text", + "test-watch": "cross-env NODE_ENV=test vitest --dir ./test --watch", "prepare": "node ./scripts/prepare.js", "lint": "eslint", "lint:fix": "eslint --fix", @@ -152,6 +152,7 @@ "@types/yargs": "^17.0.35", "@vitejs/plugin-react": "^5.1.2", "@vitest/coverage-v8": "^3.2.4", + "cross-env": "^10.1.0", "cypress": "^15.9.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", diff --git a/scripts/fix-shebang.js b/scripts/fix-shebang.js new file mode 100644 index 000000000..06cd5b8e6 --- /dev/null +++ b/scripts/fix-shebang.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const distIndexPath = path.join(__dirname, '..', 'dist', 'index.js'); + +if (fs.existsSync(distIndexPath)) { + let content = fs.readFileSync(distIndexPath, 'utf-8'); + // Replace tsx with node in the shebang (first line) + content = content.replace(/^#!.*tsx/, '#!/usr/bin/env node'); + fs.writeFileSync(distIndexPath, content); + console.log('Fixed shebang in dist/index.js'); +} else { + console.log('dist/index.js not found, skipping shebang fix'); +} diff --git a/scripts/fix-shebang.sh b/scripts/fix-shebang.sh deleted file mode 100755 index dd899a2cb..000000000 --- a/scripts/fix-shebang.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel)" -cd "$REPO_ROOT" - -# Replace tsx with node in the shebang -sed -ie '1s/tsx/node/' dist/index.js diff --git a/src/proxy/processors/push-action/preReceive.ts b/src/proxy/processors/push-action/preReceive.ts index 1c3ad36b9..9c3ad1116 100644 --- a/src/proxy/processors/push-action/preReceive.ts +++ b/src/proxy/processors/push-action/preReceive.ts @@ -15,6 +15,13 @@ const exec = async ( const step = new Step('executeExternalPreReceiveHook'); let stderrTrimmed = ''; + // Pre-receive hooks execute Unix shell scripts, which is not supported on Windows + if (process.platform === 'win32') { + step.log('Warning: Pre-receive hooks are not supported on Windows, skipping execution.'); + action.addStep(step); + return action; + } + try { const resolvedPath = path.resolve(hookFilePath); const hookDir = path.dirname(resolvedPath); diff --git a/test/ConfigLoader.test.ts b/test/ConfigLoader.test.ts index 0121b775f..e02affc37 100644 --- a/test/ConfigLoader.test.ts +++ b/test/ConfigLoader.test.ts @@ -213,7 +213,9 @@ describe('ConfigLoader', () => { } else if (process.platform === 'linux') { expect(configLoader.cacheDirPath).toContain('.cache'); } else if (process.platform === 'win32') { - expect(configLoader.cacheDirPath).toContain('AppData/Local'); + // Windows uses backslash in paths, so check for path components separately + expect(configLoader.cacheDirPath).toContain('AppData'); + expect(configLoader.cacheDirPath).toContain('Local'); } }); @@ -430,61 +432,77 @@ describe('ConfigLoader', () => { ); }); - it('should throw error if repository is a valid URL but not a git repository', async () => { - const source: ConfigurationSource = { - type: 'git', - repository: 'https://github.com/finos/made-up-test-repo.git', - path: 'proxy.config.json', - branch: 'main', - enabled: true, - }; - - await expect(configLoader.loadFromSource(source)).rejects.toThrow( - /Failed to clone repository/, - ); - }); - - it('should throw error if repository is a valid git repo but the branch does not exist', async () => { - const source: ConfigurationSource = { - type: 'git', - repository: 'https://github.com/finos/git-proxy.git', - path: 'proxy.config.json', - branch: 'branch-does-not-exist', - enabled: true, - }; - - await expect(configLoader.loadFromSource(source)).rejects.toThrow( - /Failed to checkout branch/, - ); - }); - - it('should throw error if config path was not found', async () => { - const source: ConfigurationSource = { - type: 'git', - repository: 'https://github.com/finos/git-proxy.git', - path: 'path-not-found.json', - branch: 'main', - enabled: true, - }; - - await expect(configLoader.loadFromSource(source)).rejects.toThrow( - /Configuration file not found at/, - ); - }); - - it('should throw error if config file is not valid JSON', async () => { - const source: ConfigurationSource = { - type: 'git', - repository: 'https://github.com/finos/git-proxy.git', - path: 'test/fixtures/baz.js', - branch: 'main', - enabled: true, - }; - - await expect(configLoader.loadFromSource(source)).rejects.toThrow( - /Failed to read or parse configuration file/, - ); - }); + it( + 'should throw error if repository is a valid URL but not a git repository', + async () => { + const source: ConfigurationSource = { + type: 'git', + repository: 'https://github.com/finos/made-up-test-repo.git', + path: 'proxy.config.json', + branch: 'main', + enabled: true, + }; + + await expect(configLoader.loadFromSource(source)).rejects.toThrow( + /Failed to clone repository/, + ); + }, + { timeout: 30000 }, + ); + + it( + 'should throw error if repository is a valid git repo but the branch does not exist', + async () => { + const source: ConfigurationSource = { + type: 'git', + repository: 'https://github.com/finos/git-proxy.git', + path: 'proxy.config.json', + branch: 'branch-does-not-exist', + enabled: true, + }; + + await expect(configLoader.loadFromSource(source)).rejects.toThrow( + /Failed to checkout branch/, + ); + }, + { timeout: 30000 }, + ); + + it( + 'should throw error if config path was not found', + async () => { + const source: ConfigurationSource = { + type: 'git', + repository: 'https://github.com/finos/git-proxy.git', + path: 'path-not-found.json', + branch: 'main', + enabled: true, + }; + + await expect(configLoader.loadFromSource(source)).rejects.toThrow( + /Configuration file not found at/, + ); + }, + { timeout: 30000 }, + ); + + it( + 'should throw error if config file is not valid JSON', + async () => { + const source: ConfigurationSource = { + type: 'git', + repository: 'https://github.com/finos/git-proxy.git', + path: 'test/fixtures/baz.js', + branch: 'main', + enabled: true, + }; + + await expect(configLoader.loadFromSource(source)).rejects.toThrow( + /Failed to read or parse configuration file/, + ); + }, + { timeout: 30000 }, + ); }); describe('deepMerge', () => { diff --git a/test/plugin/plugin.test.ts b/test/plugin/plugin.test.ts index 0d0afe56f..f8e60d4a6 100644 --- a/test/plugin/plugin.test.ts +++ b/test/plugin/plugin.test.ts @@ -2,20 +2,27 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { spawnSync } from 'child_process'; import { rmSync } from 'fs'; import { join } from 'path'; +import { pathToFileURL } from 'url'; import { isCompatiblePlugin, PushActionPlugin, PluginLoader } from '../../src/plugin'; +// On Windows, ESM requires file:// URLs instead of absolute paths +const toPluginPath = (filePath: string): string => { + return process.platform === 'win32' ? pathToFileURL(filePath).href : filePath; +}; + const testPackagePath = join(__dirname, '../fixtures', 'test-package'); describe('loading plugins from packages', () => { beforeAll(() => { - spawnSync('npm', ['install'], { cwd: testPackagePath, timeout: 5000 }); + // Use shell: true for cross-platform compatibility (npm.cmd on Windows) + spawnSync('npm', ['install'], { cwd: testPackagePath, timeout: 30000, shell: true }); }); describe('CommonJS syntax', () => { it( 'should load plugins that are the default export (module.exports = pluginObj)', async () => { - const loader = new PluginLoader([join(testPackagePath, 'default-export.js')]); + const loader = new PluginLoader([toPluginPath(join(testPackagePath, 'default-export.js'))]); await loader.load(); expect(loader.pushPlugins.length).toBe(1); expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).toBe(true); @@ -29,7 +36,9 @@ describe('loading plugins from packages', () => { it( 'should load multiple plugins from a module that match the plugin class (module.exports = { pluginFoo, pluginBar })', async () => { - const loader = new PluginLoader([join(testPackagePath, 'multiple-export.js')]); + const loader = new PluginLoader([ + toPluginPath(join(testPackagePath, 'multiple-export.js')), + ]); await loader.load(); expect(loader.pushPlugins.length).toBe(1); expect(loader.pullPlugins.length).toBe(1); @@ -47,7 +56,7 @@ describe('loading plugins from packages', () => { it( 'should load plugins that are subclassed from plugin classes', async () => { - const loader = new PluginLoader([join(testPackagePath, 'subclass.js')]); + const loader = new PluginLoader([toPluginPath(join(testPackagePath, 'subclass.js'))]); await loader.load(); expect(loader.pushPlugins.length).toBe(1); expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).toBe(true); @@ -63,7 +72,7 @@ describe('loading plugins from packages', () => { it( 'should load plugins that are the default export (exports default pluginObj)', async () => { - const loader = new PluginLoader([join(testPackagePath, 'esm-export.js')]); + const loader = new PluginLoader([toPluginPath(join(testPackagePath, 'esm-export.js'))]); await loader.load(); expect(loader.pushPlugins.length).toBe(1); expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).toBe(true); @@ -74,7 +83,9 @@ describe('loading plugins from packages', () => { { timeout: 10000 }, ); it('should load multiple plugins from a module that match the plugin class (exports default { pluginFoo, pluginBar })', async () => { - const loader = new PluginLoader([join(testPackagePath, 'esm-multiple-export.js')]); + const loader = new PluginLoader([ + toPluginPath(join(testPackagePath, 'esm-multiple-export.js')), + ]); await loader.load(); expect(loader.pushPlugins.length).toBe(1); expect(loader.pullPlugins.length).toBe(1); @@ -87,7 +98,7 @@ describe('loading plugins from packages', () => { ).toBe(true); }); it('should load plugins that are subclassed from plugin classes (exports default class DummyPlugin extends PushActionPlugin {})', async () => { - const loader = new PluginLoader([join(testPackagePath, 'esm-subclass.js')]); + const loader = new PluginLoader([toPluginPath(join(testPackagePath, 'esm-subclass.js'))]); await loader.load(); expect(loader.pushPlugins.length).toBe(1); expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).toBe(true); @@ -100,7 +111,7 @@ describe('loading plugins from packages', () => { it( 'should not load plugins that are not valid modules', async () => { - const loader = new PluginLoader([join(__dirname, './dummy.js')]); + const loader = new PluginLoader([toPluginPath(join(__dirname, './dummy.js'))]); await loader.load(); expect(loader.pushPlugins.length).toBe(0); expect(loader.pullPlugins.length).toBe(0); @@ -111,7 +122,7 @@ describe('loading plugins from packages', () => { it( 'should not load plugins that are not extended from plugin objects', async () => { - const loader = new PluginLoader([join(__dirname, './fixtures/baz.js')]); + const loader = new PluginLoader([toPluginPath(join(__dirname, './fixtures/baz.js'))]); await loader.load(); expect(loader.pushPlugins.length).toBe(0); expect(loader.pullPlugins.length).toBe(0); @@ -120,7 +131,8 @@ describe('loading plugins from packages', () => { ); afterAll(() => { - rmSync(join(testPackagePath, 'node_modules'), { recursive: true }); + // Use force: true to avoid error if node_modules doesn't exist + rmSync(join(testPackagePath, 'node_modules'), { recursive: true, force: true }); }); }); diff --git a/test/preReceive/preReceive.test.ts b/test/preReceive/preReceive.test.ts index bc8f3a416..eec7e5d17 100644 --- a/test/preReceive/preReceive.test.ts +++ b/test/preReceive/preReceive.test.ts @@ -6,7 +6,8 @@ import { exec } from '../../src/proxy/processors/push-action/preReceive'; // TODO: Replace with memfs to prevent test pollution issues vi.mock('fs', { spy: true }); -describe('Pre-Receive Hook Execution', () => { +// Pre-receive hooks execute Unix shell scripts, which is not supported on Windows +describe.skipIf(process.platform === 'win32')('Pre-Receive Hook Execution', () => { let action: any; let req: any; diff --git a/test/processors/writePack.test.ts b/test/processors/writePack.test.ts index 85d948243..68ede8dae 100644 --- a/test/processors/writePack.test.ts +++ b/test/processors/writePack.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import path from 'path'; import { Action, Step } from '../../src/proxy/actions'; import * as childProcess from 'child_process'; import * as fs from 'fs'; @@ -51,7 +52,8 @@ describe('writePack', () => { 1234567890, 'https://github.com/finos/git-proxy.git', ); - action.proxyGitPath = '/path/to'; + // Use path.join for cross-platform compatibility + action.proxyGitPath = path.join('path', 'to'); action.repoName = 'repo'; }); @@ -66,14 +68,14 @@ describe('writePack', () => { 1, 'git', ['config', 'receive.unpackLimit', '0'], - expect.objectContaining({ cwd: '/path/to/repo' }), + expect.objectContaining({ cwd: path.join('path', 'to', 'repo') }), ); expect(spawnSyncMock).toHaveBeenNthCalledWith( 2, 'git', ['receive-pack', 'repo'], expect.objectContaining({ - cwd: '/path/to', + cwd: path.join('path', 'to'), input: 'pack data', }), );