mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-04-05 07:17:52 +10:00
wip major changes
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* Unit tests for chrome_extension_utils.js
|
||||
*
|
||||
* Run with: npm test
|
||||
* Or: node --test tests/test_chrome_extension_utils.js
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { describe, it, before, after, beforeEach, afterEach } = require('node:test');
|
||||
|
||||
// Import module under test
|
||||
const extensionUtils = require('../chrome_extension_utils.js');
|
||||
|
||||
// Test fixtures
|
||||
const TEST_DIR = path.join(__dirname, '.test_fixtures');
|
||||
const TEST_EXTENSIONS_DIR = path.join(TEST_DIR, 'chrome_extensions');
|
||||
|
||||
describe('chrome_extension_utils', () => {
|
||||
before(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(TEST_DIR)) {
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Cleanup test directory
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('getExtensionId', () => {
|
||||
it('should compute extension ID from path', () => {
|
||||
const testPath = '/path/to/extension';
|
||||
const extensionId = extensionUtils.getExtensionId(testPath);
|
||||
|
||||
assert.strictEqual(typeof extensionId, 'string');
|
||||
assert.strictEqual(extensionId.length, 32);
|
||||
// Should only contain lowercase letters a-p
|
||||
assert.match(extensionId, /^[a-p]+$/);
|
||||
});
|
||||
|
||||
it('should compute ID even for non-existent paths', () => {
|
||||
const testPath = '/nonexistent/path';
|
||||
const extensionId = extensionUtils.getExtensionId(testPath);
|
||||
|
||||
// Should still compute an ID from the path string
|
||||
assert.strictEqual(typeof extensionId, 'string');
|
||||
assert.strictEqual(extensionId.length, 32);
|
||||
assert.match(extensionId, /^[a-p]+$/);
|
||||
});
|
||||
|
||||
it('should return consistent ID for same path', () => {
|
||||
const testPath = '/path/to/extension';
|
||||
const id1 = extensionUtils.getExtensionId(testPath);
|
||||
const id2 = extensionUtils.getExtensionId(testPath);
|
||||
|
||||
assert.strictEqual(id1, id2);
|
||||
});
|
||||
|
||||
it('should return different IDs for different paths', () => {
|
||||
const path1 = '/path/to/extension1';
|
||||
const path2 = '/path/to/extension2';
|
||||
const id1 = extensionUtils.getExtensionId(path1);
|
||||
const id2 = extensionUtils.getExtensionId(path2);
|
||||
|
||||
assert.notStrictEqual(id1, id2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadExtensionManifest', () => {
|
||||
beforeEach(() => {
|
||||
// Create test extension directory with manifest
|
||||
const testExtDir = path.join(TEST_DIR, 'test_extension');
|
||||
fs.mkdirSync(testExtDir, { recursive: true });
|
||||
|
||||
const manifest = {
|
||||
manifest_version: 3,
|
||||
name: "Test Extension",
|
||||
version: "1.0.0"
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(testExtDir, 'manifest.json'),
|
||||
JSON.stringify(manifest)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Cleanup test extension
|
||||
const testExtDir = path.join(TEST_DIR, 'test_extension');
|
||||
if (fs.existsSync(testExtDir)) {
|
||||
fs.rmSync(testExtDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should load valid manifest.json', () => {
|
||||
const testExtDir = path.join(TEST_DIR, 'test_extension');
|
||||
const manifest = extensionUtils.loadExtensionManifest(testExtDir);
|
||||
|
||||
assert.notStrictEqual(manifest, null);
|
||||
assert.strictEqual(manifest.manifest_version, 3);
|
||||
assert.strictEqual(manifest.name, "Test Extension");
|
||||
assert.strictEqual(manifest.version, "1.0.0");
|
||||
});
|
||||
|
||||
it('should return null for missing manifest', () => {
|
||||
const nonExistentDir = path.join(TEST_DIR, 'nonexistent');
|
||||
const manifest = extensionUtils.loadExtensionManifest(nonExistentDir);
|
||||
|
||||
assert.strictEqual(manifest, null);
|
||||
});
|
||||
|
||||
it('should handle invalid JSON gracefully', () => {
|
||||
const testExtDir = path.join(TEST_DIR, 'invalid_extension');
|
||||
fs.mkdirSync(testExtDir, { recursive: true });
|
||||
|
||||
// Write invalid JSON
|
||||
fs.writeFileSync(
|
||||
path.join(testExtDir, 'manifest.json'),
|
||||
'invalid json content'
|
||||
);
|
||||
|
||||
const manifest = extensionUtils.loadExtensionManifest(testExtDir);
|
||||
|
||||
assert.strictEqual(manifest, null);
|
||||
|
||||
// Cleanup
|
||||
fs.rmSync(testExtDir, { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExtensionLaunchArgs', () => {
|
||||
it('should return empty array for no extensions', () => {
|
||||
const args = extensionUtils.getExtensionLaunchArgs([]);
|
||||
|
||||
assert.deepStrictEqual(args, []);
|
||||
});
|
||||
|
||||
it('should generate correct launch args for single extension', () => {
|
||||
const extensions = [{
|
||||
webstore_id: 'abcd1234',
|
||||
unpacked_path: '/path/to/extension'
|
||||
}];
|
||||
|
||||
const args = extensionUtils.getExtensionLaunchArgs(extensions);
|
||||
|
||||
assert.strictEqual(args.length, 4);
|
||||
assert.strictEqual(args[0], '--load-extension=/path/to/extension');
|
||||
assert.strictEqual(args[1], '--allowlisted-extension-id=abcd1234');
|
||||
assert.strictEqual(args[2], '--allow-legacy-extension-manifests');
|
||||
assert.strictEqual(args[3], '--disable-extensions-auto-update');
|
||||
});
|
||||
|
||||
it('should generate correct launch args for multiple extensions', () => {
|
||||
const extensions = [
|
||||
{ webstore_id: 'ext1', unpacked_path: '/path/ext1' },
|
||||
{ webstore_id: 'ext2', unpacked_path: '/path/ext2' },
|
||||
{ webstore_id: 'ext3', unpacked_path: '/path/ext3' }
|
||||
];
|
||||
|
||||
const args = extensionUtils.getExtensionLaunchArgs(extensions);
|
||||
|
||||
assert.strictEqual(args.length, 4);
|
||||
assert.strictEqual(args[0], '--load-extension=/path/ext1,/path/ext2,/path/ext3');
|
||||
assert.strictEqual(args[1], '--allowlisted-extension-id=ext1,ext2,ext3');
|
||||
});
|
||||
|
||||
it('should handle extensions with id instead of webstore_id', () => {
|
||||
const extensions = [{
|
||||
id: 'computed_id',
|
||||
unpacked_path: '/path/to/extension'
|
||||
}];
|
||||
|
||||
const args = extensionUtils.getExtensionLaunchArgs(extensions);
|
||||
|
||||
assert.strictEqual(args[1], '--allowlisted-extension-id=computed_id');
|
||||
});
|
||||
|
||||
it('should filter out extensions without paths', () => {
|
||||
const extensions = [
|
||||
{ webstore_id: 'ext1', unpacked_path: '/path/ext1' },
|
||||
{ webstore_id: 'ext2', unpacked_path: null },
|
||||
{ webstore_id: 'ext3', unpacked_path: '/path/ext3' }
|
||||
];
|
||||
|
||||
const args = extensionUtils.getExtensionLaunchArgs(extensions);
|
||||
|
||||
assert.strictEqual(args[0], '--load-extension=/path/ext1,/path/ext3');
|
||||
assert.strictEqual(args[1], '--allowlisted-extension-id=ext1,ext3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadOrInstallExtension', () => {
|
||||
beforeEach(() => {
|
||||
// Create test extensions directory
|
||||
if (!fs.existsSync(TEST_EXTENSIONS_DIR)) {
|
||||
fs.mkdirSync(TEST_EXTENSIONS_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Cleanup test extensions directory
|
||||
if (fs.existsSync(TEST_EXTENSIONS_DIR)) {
|
||||
fs.rmSync(TEST_EXTENSIONS_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error if neither webstore_id nor unpacked_path provided', async () => {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await extensionUtils.loadOrInstallExtension({}, TEST_EXTENSIONS_DIR);
|
||||
},
|
||||
/Extension must have either/
|
||||
);
|
||||
});
|
||||
|
||||
it('should set correct default values for extension metadata', async () => {
|
||||
const input = {
|
||||
webstore_id: 'test123',
|
||||
name: 'test_extension'
|
||||
};
|
||||
|
||||
// Mock the installation to avoid actual download
|
||||
const originalInstall = extensionUtils.installExtension;
|
||||
extensionUtils.installExtension = async () => {
|
||||
// Create fake manifest
|
||||
const extDir = path.join(TEST_EXTENSIONS_DIR, 'test123__test_extension');
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'manifest.json'),
|
||||
JSON.stringify({ version: '1.0.0' })
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
const ext = await extensionUtils.loadOrInstallExtension(input, TEST_EXTENSIONS_DIR);
|
||||
|
||||
// Restore original
|
||||
extensionUtils.installExtension = originalInstall;
|
||||
|
||||
assert.strictEqual(ext.webstore_id, 'test123');
|
||||
assert.strictEqual(ext.name, 'test_extension');
|
||||
assert.ok(ext.webstore_url.includes(ext.webstore_id));
|
||||
assert.ok(ext.crx_url.includes(ext.webstore_id));
|
||||
assert.ok(ext.crx_path.includes('test123__test_extension.crx'));
|
||||
assert.ok(ext.unpacked_path.includes('test123__test_extension'));
|
||||
});
|
||||
|
||||
it('should detect version from manifest after installation', async () => {
|
||||
const input = {
|
||||
webstore_id: 'test456',
|
||||
name: 'versioned_extension'
|
||||
};
|
||||
|
||||
// Create pre-installed extension
|
||||
const extDir = path.join(TEST_EXTENSIONS_DIR, 'test456__versioned_extension');
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'manifest.json'),
|
||||
JSON.stringify({
|
||||
manifest_version: 3,
|
||||
name: "Versioned Extension",
|
||||
version: "2.5.1"
|
||||
})
|
||||
);
|
||||
|
||||
const ext = await extensionUtils.loadOrInstallExtension(input, TEST_EXTENSIONS_DIR);
|
||||
|
||||
assert.strictEqual(ext.version, '2.5.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTargetExtension', () => {
|
||||
it('should identify extension targets by URL', async () => {
|
||||
// Mock Puppeteer target
|
||||
const mockTarget = {
|
||||
type: () => 'service_worker',
|
||||
url: () => 'chrome-extension://abcdefgh/background.js',
|
||||
worker: async () => null,
|
||||
page: async () => null
|
||||
};
|
||||
|
||||
const result = await extensionUtils.isTargetExtension(mockTarget);
|
||||
|
||||
assert.strictEqual(result.target_is_extension, true);
|
||||
assert.strictEqual(result.target_is_bg, true);
|
||||
assert.strictEqual(result.extension_id, 'abcdefgh');
|
||||
});
|
||||
|
||||
it('should not identify non-extension targets', async () => {
|
||||
const mockTarget = {
|
||||
type: () => 'page',
|
||||
url: () => 'https://example.com',
|
||||
worker: async () => null,
|
||||
page: async () => null
|
||||
};
|
||||
|
||||
const result = await extensionUtils.isTargetExtension(mockTarget);
|
||||
|
||||
assert.strictEqual(result.target_is_extension, false);
|
||||
assert.strictEqual(result.target_is_bg, false);
|
||||
assert.strictEqual(result.extension_id, null);
|
||||
});
|
||||
|
||||
it('should handle closed targets gracefully', async () => {
|
||||
const mockTarget = {
|
||||
type: () => { throw new Error('No target with given id found'); },
|
||||
url: () => { throw new Error('No target with given id found'); },
|
||||
worker: async () => { throw new Error('No target with given id found'); },
|
||||
page: async () => { throw new Error('No target with given id found'); }
|
||||
};
|
||||
|
||||
const result = await extensionUtils.isTargetExtension(mockTarget);
|
||||
|
||||
assert.strictEqual(result.target_type, 'closed');
|
||||
assert.strictEqual(result.target_url, 'about:closed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Run tests if executed directly
|
||||
if (require.main === module) {
|
||||
console.log('Run tests with: npm test');
|
||||
console.log('Or: node --test tests/test_chrome_extension_utils.js');
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Unit tests for chrome_extension_utils.js
|
||||
|
||||
Tests invoke the script as an external process and verify outputs/side effects.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).parent.parent / "chrome_extension_utils.js"
|
||||
|
||||
|
||||
def test_script_exists():
|
||||
"""Verify the script file exists and is executable via node"""
|
||||
assert SCRIPT_PATH.exists(), f"Script not found: {SCRIPT_PATH}"
|
||||
|
||||
|
||||
def test_get_extension_id():
|
||||
"""Test extension ID computation from path"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
test_path = "/path/to/extension"
|
||||
|
||||
# Run script with test path
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionId", test_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
extension_id = result.stdout.strip()
|
||||
|
||||
# Should return 32-character ID with only letters a-p
|
||||
assert len(extension_id) == 32
|
||||
assert all(c in 'abcdefghijklmnop' for c in extension_id)
|
||||
|
||||
|
||||
def test_get_extension_id_consistency():
|
||||
"""Test that same path produces same ID"""
|
||||
test_path = "/path/to/extension"
|
||||
|
||||
result1 = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionId", test_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
result2 = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionId", test_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result1.returncode == 0
|
||||
assert result2.returncode == 0
|
||||
assert result1.stdout.strip() == result2.stdout.strip()
|
||||
|
||||
|
||||
def test_get_extension_id_different_paths():
|
||||
"""Test that different paths produce different IDs"""
|
||||
result1 = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionId", "/path1"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
result2 = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionId", "/path2"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result1.returncode == 0
|
||||
assert result2.returncode == 0
|
||||
assert result1.stdout.strip() != result2.stdout.strip()
|
||||
|
||||
|
||||
def test_load_extension_manifest():
|
||||
"""Test loading extension manifest.json"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ext_dir = Path(tmpdir) / "test_extension"
|
||||
ext_dir.mkdir()
|
||||
|
||||
# Create manifest
|
||||
manifest = {
|
||||
"manifest_version": 3,
|
||||
"name": "Test Extension",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
(ext_dir / "manifest.json").write_text(json.dumps(manifest))
|
||||
|
||||
# Load manifest via script
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "loadExtensionManifest", str(ext_dir)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
loaded = json.loads(result.stdout)
|
||||
|
||||
assert loaded["manifest_version"] == 3
|
||||
assert loaded["name"] == "Test Extension"
|
||||
assert loaded["version"] == "1.0.0"
|
||||
|
||||
|
||||
def test_load_extension_manifest_missing():
|
||||
"""Test loading manifest from non-existent directory"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
nonexistent = Path(tmpdir) / "nonexistent"
|
||||
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "loadExtensionManifest", str(nonexistent)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Should return null/empty for missing manifest
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() in ("null", "")
|
||||
|
||||
|
||||
def test_load_extension_manifest_invalid_json():
|
||||
"""Test handling of invalid JSON in manifest"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ext_dir = Path(tmpdir) / "test_extension"
|
||||
ext_dir.mkdir()
|
||||
|
||||
# Write invalid JSON
|
||||
(ext_dir / "manifest.json").write_text("invalid json content")
|
||||
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "loadExtensionManifest", str(ext_dir)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Should handle gracefully
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() in ("null", "")
|
||||
|
||||
|
||||
def test_get_extension_launch_args_empty():
|
||||
"""Test launch args with no extensions"""
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionLaunchArgs", "[]"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
args = json.loads(result.stdout)
|
||||
assert args == []
|
||||
|
||||
|
||||
def test_get_extension_launch_args_single():
|
||||
"""Test launch args with single extension"""
|
||||
extensions = [{
|
||||
"webstore_id": "abcd1234",
|
||||
"unpacked_path": "/path/to/extension"
|
||||
}]
|
||||
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionLaunchArgs", json.dumps(extensions)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
args = json.loads(result.stdout)
|
||||
|
||||
assert len(args) == 4
|
||||
assert args[0] == "--load-extension=/path/to/extension"
|
||||
assert args[1] == "--allowlisted-extension-id=abcd1234"
|
||||
assert args[2] == "--allow-legacy-extension-manifests"
|
||||
assert args[3] == "--disable-extensions-auto-update"
|
||||
|
||||
|
||||
def test_get_extension_launch_args_multiple():
|
||||
"""Test launch args with multiple extensions"""
|
||||
extensions = [
|
||||
{"webstore_id": "ext1", "unpacked_path": "/path/ext1"},
|
||||
{"webstore_id": "ext2", "unpacked_path": "/path/ext2"},
|
||||
{"webstore_id": "ext3", "unpacked_path": "/path/ext3"}
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionLaunchArgs", json.dumps(extensions)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
args = json.loads(result.stdout)
|
||||
|
||||
assert args[0] == "--load-extension=/path/ext1,/path/ext2,/path/ext3"
|
||||
assert args[1] == "--allowlisted-extension-id=ext1,ext2,ext3"
|
||||
|
||||
|
||||
def test_get_extension_launch_args_filter_null_paths():
|
||||
"""Test that extensions without paths are filtered out"""
|
||||
extensions = [
|
||||
{"webstore_id": "ext1", "unpacked_path": "/path/ext1"},
|
||||
{"webstore_id": "ext2", "unpacked_path": None},
|
||||
{"webstore_id": "ext3", "unpacked_path": "/path/ext3"}
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
["node", str(SCRIPT_PATH), "getExtensionLaunchArgs", json.dumps(extensions)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
args = json.loads(result.stdout)
|
||||
|
||||
assert args[0] == "--load-extension=/path/ext1,/path/ext3"
|
||||
assert args[1] == "--allowlisted-extension-id=ext1,ext3"
|
||||
Reference in New Issue
Block a user