diff --git a/src/phoenix/test/test-git-detection.js b/src/phoenix/test/test-git-detection.js new file mode 100644 index 000000000..976ca48e2 --- /dev/null +++ b/src/phoenix/test/test-git-detection.js @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { JSDOM } from 'jsdom'; + +// Function to test +async function hasGitDirectory(items) { + // Case 1: Single Puter path + if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { + const stat = await global.puter.fs.stat(items); + if (stat.is_dir) { + const files = await global.puter.fs.readdir(items); + return files.some(file => file.name === '.git' && file.is_dir); + } + return false; + } + + // Case 2: Array of Puter items + if (Array.isArray(items) && items[0]?.uid) { + return items.some(item => item.name === '.git' && item.is_dir); + } + + // Case 3: Local items (DataTransferItems) + if (Array.isArray(items)) { + for (let item of items) { + if (item.fullPath?.includes('/.git/') || + item.path?.includes('/.git/') || + item.filepath?.includes('/.git/')) { + return true; + } + } + } + + return false; +} + +describe('hasGitDirectory', () => { + // Mock puter.fs for testing + const mockPuterFS = { + stat: async (path) => ({ + is_dir: path.endsWith('dir') + }), + readdir: async (path) => { + if (path === '/path/to/git/dir') { + return [ + { name: '.git', is_dir: true }, + { name: 'src', is_dir: true } + ]; + } + return [ + { name: 'src', is_dir: true }, + { name: 'test', is_dir: true } + ]; + } + }; + + beforeEach(() => { + // Set up global puter object before each test + global.puter = { fs: mockPuterFS }; + }); + + afterEach(() => { + // Clean up global puter object after each test + delete global.puter; + }); + + describe('Case 1: Single Puter path', () => { + it('should return true for directory containing .git', async () => { + const result = await hasGitDirectory('/path/to/git/dir'); + assert.strictEqual(result, true); + }); + + it('should return false for directory without .git', async () => { + const result = await hasGitDirectory('/path/to/normal/dir'); + assert.strictEqual(result, false); + }); + + it('should return false for non-directory path', async () => { + const result = await hasGitDirectory('/path/to/file'); + assert.strictEqual(result, false); + }); + }); + + describe('Case 2: Array of Puter items', () => { + it('should return true when .git directory is present', async () => { + const items = [ + { uid: '1', name: 'src', is_dir: true }, + { uid: '2', name: '.git', is_dir: true }, + { uid: '3', name: 'test', is_dir: true } + ]; + const result = await hasGitDirectory(items); + assert.strictEqual(result, true); + }); + + it('should return false when no .git directory is present', async () => { + const items = [ + { uid: '1', name: 'src', is_dir: true }, + { uid: '2', name: 'test', is_dir: true } + ]; + const result = await hasGitDirectory(items); + assert.strictEqual(result, false); + }); + }); + + describe('Case 3: Local items (DataTransferItems)', () => { + it('should return true when path contains .git directory', async () => { + const items = [ + { fullPath: '/project/.git/config' }, + { fullPath: '/project/src/index.js' } + ]; + const result = await hasGitDirectory(items); + assert.strictEqual(result, true); + }); + + it('should return true when using alternative path properties', async () => { + const items = [ + { path: '/project/.git/config' }, + { filepath: '/project/src/index.js' } + ]; + const result = await hasGitDirectory(items); + assert.strictEqual(result, true); + }); + + it('should return false when no .git directory is present', async () => { + const items = [ + { fullPath: '/project/src/index.js' }, + { fullPath: '/project/test/test.js' } + ]; + const result = await hasGitDirectory(items); + assert.strictEqual(result, false); + }); + }); + + describe('Edge cases', () => { + it('should handle empty array input', async () => { + const result = await hasGitDirectory([]); + assert.strictEqual(result, false); + }); + + it('should handle invalid input', async () => { + const result = await hasGitDirectory(null); + assert.strictEqual(result, false); + }); + + it('should handle mixed path formats', async () => { + const items = [ + { fullPath: '/project/src/index.js' }, + { path: '/project/.git/config' }, + { filepath: '/project/test/test.js' } + ]; + const result = await hasGitDirectory(items); + assert.strictEqual(result, true); + }); + }); +}); + + diff --git a/src/phoenix/test/test-git-warning-dialog.js b/src/phoenix/test/test-git-warning-dialog.js new file mode 100644 index 000000000..b0afde3df --- /dev/null +++ b/src/phoenix/test/test-git-warning-dialog.js @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { JSDOM } from 'jsdom'; + +// Function to test +async function showGitWarningDialog() { + try { + // Check if the user has chosen to skip the warning + const skipWarning = await global.puter.kv.get('skip-git-warning'); + + // Log retrieved value for debugging + console.log('Retrieved skip-git-warning:', skipWarning); + + // If the user opted to skip the warning, proceed without showing it + if (skipWarning === true) { + return true; + } + } catch (error) { + console.error('Error accessing KV store:', error); + // If KV store access fails, fall back to showing the dialog + } + + // Create the modal dialog + const modal = document.createElement('div'); + modal.innerHTML = ` +
+

Warning: Git Repository Detected

+

A .git directory was found in your deployment files. Deploying .git directories may:

+
    +
  • Expose sensitive information like commit history and configuration
  • +
  • Significantly increase deployment size
  • +
+
+ + +
+
+ + +
+
+
+ `; + document.body.appendChild(modal); + + return new Promise((resolve) => { + // Handle "Continue Deployment" + document.getElementById('continue-deployment').addEventListener('click', async () => { + try { + const skipChecked = document.getElementById('skip-git-warning')?.checked; + if (skipChecked) { + console.log("Saving 'skip-git-warning' preference as true"); + await global.puter.kv.set('skip-git-warning', true); + } + } catch (error) { + console.error('Error saving user preference to KV store:', error); + } finally { + document.body.removeChild(modal); + resolve(true); // Continue deployment + } + }); + + // Handle "Cancel Deployment" + document.getElementById('cancel-deployment').addEventListener('click', () => { + document.body.removeChild(modal); + resolve(false); // Cancel deployment + }); + }); +} + +describe('showGitWarningDialog', () => { + let dom; + let consoleLogs = []; + let consoleErrors = []; + + // Mock console methods + const mockConsole = { + log: (msg) => consoleLogs.push(msg), + error: (msg) => consoleErrors.push(msg) + }; + + // Mock puter.kv + const mockPuterKV = { + get: async () => false, + set: async () => {} + }; + + beforeEach(() => { + // Set up JSDOM with all required features + dom = new JSDOM(``, { + url: 'http://localhost', + runScripts: 'dangerously', + resources: 'usable', + pretendToBeVisual: true + }); + + // Set up global objects + global.document = dom.window.document; + global.window = dom.window; + global.HTMLElement = dom.window.HTMLElement; + global.Element = dom.window.Element; + global.Node = dom.window.Node; + global.navigator = dom.window.navigator; + global.console = { ...console, ...mockConsole }; + global.puter = { kv: mockPuterKV }; + + // Reset console logs + consoleLogs = []; + consoleErrors = []; + }); + + afterEach(() => { + // Clean up + dom.window.document.body.innerHTML = ''; + }); + + describe('Skip Warning Behavior', () => { + it('should skip dialog if warning is disabled', async () => { + global.puter.kv.get = async () => true; + + const result = await showGitWarningDialog(); + + assert.strictEqual(result, true); + assert.strictEqual(dom.window.document.body.children.length, 0); + assert.ok(consoleLogs.some(log => log.includes('Retrieved skip-git-warning'))); + }); + + it('should show dialog if warning is not disabled', async () => { + global.puter.kv.get = async () => false; + + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + // Check if dialog is shown + assert.strictEqual(dom.window.document.body.children.length, 1); + assert.ok(dom.window.document.querySelector('h3').textContent.includes('Git Repository Detected')); + + // Simulate clicking "Cancel" to resolve the promise + dom.window.document.getElementById('cancel-deployment').click(); + const result = await dialogPromise; + assert.strictEqual(result, false); + }); + + it('should handle KV store error gracefully', async () => { + global.puter.kv.get = async () => { + throw new Error('KV store error'); + }; + + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + // Check if dialog is shown + assert.strictEqual(dom.window.document.body.children.length, 1); + assert.ok(consoleErrors.some(error => error.includes('Error accessing KV store'))); + + // Cleanup + dom.window.document.getElementById('cancel-deployment').click(); + await dialogPromise; + }); + }); + + describe('User Interaction', () => { + it('should save preference when checkbox is checked', async () => { + let kvSetCalled = false; + global.puter.kv.set = async (key, value) => { + assert.strictEqual(key, 'skip-git-warning'); + assert.strictEqual(value, true); + kvSetCalled = true; + }; + + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + const checkbox = dom.window.document.getElementById('skip-git-warning'); + checkbox.checked = true; + + dom.window.document.getElementById('continue-deployment').click(); + const result = await dialogPromise; + + assert.strictEqual(result, true); + assert.ok(kvSetCalled); + assert.ok(consoleLogs.some(log => log.includes("Saving 'skip-git-warning' preference"))); + }); + + it('should not save preference when checkbox is unchecked', async () => { + let kvSetCalled = false; + global.puter.kv.set = async () => { + kvSetCalled = true; + }; + + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + dom.window.document.getElementById('continue-deployment').click(); + await dialogPromise; + + assert.strictEqual(kvSetCalled, false); + }); + + it('should handle KV set error gracefully', async () => { + global.puter.kv.set = async () => { + throw new Error('KV set error'); + }; + + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + const checkbox = dom.window.document.getElementById('skip-git-warning'); + checkbox.checked = true; + + dom.window.document.getElementById('continue-deployment').click(); + const result = await dialogPromise; + + assert.strictEqual(result, true); + assert.ok(consoleErrors.some(error => error.includes('Error saving user preference'))); + }); + }); + + describe('Dialog UI', () => { + it('should create dialog with all required elements', async () => { + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + assert.ok(dom.window.document.querySelector('h3')); + assert.ok(dom.window.document.querySelector('ul')); + assert.ok(dom.window.document.getElementById('skip-git-warning')); + assert.ok(dom.window.document.getElementById('cancel-deployment')); + assert.ok(dom.window.document.getElementById('continue-deployment')); + + // Cleanup + dom.window.document.getElementById('cancel-deployment').click(); + await dialogPromise; + }); + + it('should remove dialog after interaction', async () => { + const dialogPromise = showGitWarningDialog(); + + // Wait for the next tick to allow DOM updates + await new Promise(resolve => setTimeout(resolve, 0)); + + assert.strictEqual(dom.window.document.body.children.length, 1); + + dom.window.document.getElementById('continue-deployment').click(); + await dialogPromise; + + assert.strictEqual(dom.window.document.body.children.length, 0); + }); + }); +});