3404 lines
133 KiB
C#
3404 lines
133 KiB
C#
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Windows;
|
|
using Paca.App;
|
|
using Paca.Browser;
|
|
using Paca.Browser.Extensions;
|
|
using Xunit;
|
|
|
|
namespace Paca.Tests.Browser;
|
|
|
|
/// <summary>
|
|
/// 크롬 확장 프로그램 엔진 대통합 파이프라인 (Manifest V2/V3, WebStore API, Background Script, CRX3, 샌드박스, 105개 극한 검증).
|
|
/// </summary>
|
|
|
|
|
|
public sealed class ExtensionCoreTests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
|
|
public ExtensionCoreTests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), "VD_ExtTests_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(_tempDir))
|
|
Directory.Delete(_tempDir, true);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CrxPackageExtractor_ExtractCrx3_Success()
|
|
{
|
|
// Arrange: Create a valid CRX3 stream
|
|
var zipBytes = CreateSampleZipBytes("My Extension", "1.0.0", "A great chrome extension");
|
|
var headerBytes = new byte[] { 0x08, 0x01, 0x12, 0x04, 0x74, 0x65, 0x73, 0x74 }; // dummy header
|
|
using var ms = new MemoryStream();
|
|
using var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true);
|
|
bw.Write(new byte[] { 0x43, 0x72, 0x32, 0x34 }); // "Cr24"
|
|
bw.Write((uint)3); // version 3
|
|
bw.Write((uint)headerBytes.Length); // header length
|
|
bw.Write(headerBytes); // header
|
|
bw.Write(zipBytes); // zip payload
|
|
bw.Flush();
|
|
ms.Position = 0;
|
|
|
|
var targetDir = Path.Combine(_tempDir, "crx3_output");
|
|
|
|
// Act
|
|
var manifest = await CrxPackageExtractor.ExtractAsync(ms, targetDir);
|
|
|
|
// Assert
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("My Extension", manifest.Name);
|
|
Assert.Equal("1.0.0", manifest.Version);
|
|
Assert.Equal("A great chrome extension", manifest.Description);
|
|
Assert.True(File.Exists(Path.Combine(targetDir, "manifest.json")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CrxPackageExtractor_ExtractCrx2_Success()
|
|
{
|
|
// Arrange: Create a valid CRX2 stream
|
|
var zipBytes = CreateSampleZipBytes("CRX2 Extension", "2.0.0", "Legacy crx format");
|
|
var pubKey = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
|
var sig = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };
|
|
using var ms = new MemoryStream();
|
|
using var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true);
|
|
bw.Write(new byte[] { 0x43, 0x72, 0x32, 0x34 }); // "Cr24"
|
|
bw.Write((uint)2); // version 2
|
|
bw.Write((uint)pubKey.Length); // pubkey len
|
|
bw.Write((uint)sig.Length); // sig len
|
|
bw.Write(pubKey);
|
|
bw.Write(sig);
|
|
bw.Write(zipBytes);
|
|
bw.Flush();
|
|
ms.Position = 0;
|
|
|
|
var targetDir = Path.Combine(_tempDir, "crx2_output");
|
|
|
|
// Act
|
|
var manifest = await CrxPackageExtractor.ExtractAsync(ms, targetDir);
|
|
|
|
// Assert
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("CRX2 Extension", manifest.Name);
|
|
Assert.Equal("2.0.0", manifest.Version);
|
|
Assert.True(File.Exists(Path.Combine(targetDir, "manifest.json")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CrxPackageExtractor_ExtractPlainZip_Success()
|
|
{
|
|
// Arrange
|
|
var zipBytes = CreateSampleZipBytes("Zip Extension", "3.1.4", "Plain zip packaged");
|
|
using var ms = new MemoryStream(zipBytes);
|
|
var targetDir = Path.Combine(_tempDir, "zip_output");
|
|
|
|
// Act
|
|
var manifest = await CrxPackageExtractor.ExtractAsync(ms, targetDir);
|
|
|
|
// Assert
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("Zip Extension", manifest.Name);
|
|
Assert.Equal("3.1.4", manifest.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CrxPackageExtractor_InvalidBytes_ThrowsInvalidDataException()
|
|
{
|
|
var garbage = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
|
|
using var ms = new MemoryStream(garbage);
|
|
var targetDir = Path.Combine(_tempDir, "invalid_output");
|
|
|
|
await Assert.ThrowsAsync<InvalidDataException>(async () =>
|
|
{
|
|
await CrxPackageExtractor.ExtractAsync(ms, targetDir);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ManifestParser_ResolvesI18nMessages()
|
|
{
|
|
// Arrange
|
|
var extFolder = Path.Combine(_tempDir, "i18n_ext");
|
|
var localesFolder = Path.Combine(extFolder, "_locales", "ko");
|
|
Directory.CreateDirectory(localesFolder);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "__MSG_extensionName__",
|
|
"version": "1.2.0",
|
|
"description": "__MSG_extensionDescription__",
|
|
"default_locale": "ko"
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), manifestJson, Encoding.UTF8);
|
|
|
|
var messagesJson = """
|
|
{
|
|
"extensionName": {
|
|
"message": "스마트 비디오 다운로더 확장"
|
|
},
|
|
"extensionDescription": {
|
|
"message": "웹 영상 감지 및 다운로드 도우미"
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(localesFolder, "messages.json"), messagesJson, Encoding.UTF8);
|
|
|
|
// Act
|
|
var manifest = ExtensionManifest.LoadFromDirectory(extFolder);
|
|
|
|
// Assert
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("스마트 비디오 다운로더 확장", manifest.Name);
|
|
Assert.Equal("웹 영상 감지 및 다운로드 도우미", manifest.Description);
|
|
Assert.Equal("1.2.0", manifest.Version);
|
|
Assert.Equal(3, manifest.ManifestVersion);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm")]
|
|
[InlineData("https://chromewebstore.google.com/detail/cjpalhdlnbpafiamejdnhcphjbkeiagm?hl=ko", "cjpalhdlnbpafiamejdnhcphjbkeiagm")]
|
|
[InlineData("https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo", "dhdgffkkebhmkfjojejmpbldmpobfkfo")]
|
|
[InlineData("cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm")]
|
|
[InlineData(" DHDGFFKKEBHMKFJOJEJMPBLDMPOBFKFO ", "dhdgffkkebhmkfjojejmpbldmpobfkfo")]
|
|
public void ChromeWebStoreClient_ExtractExtensionId_Valid(string input, string expectedId)
|
|
{
|
|
var id = ChromeWebStoreClient.ExtractExtensionId(input);
|
|
Assert.Equal(expectedId, id);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://google.com")]
|
|
[InlineData("https://chromewebstore.google.com/category/extensions")]
|
|
[InlineData("invalid_short_id")]
|
|
[InlineData("12345678901234567890123456789012")] // digits are not [a-p]
|
|
public void ChromeWebStoreClient_ExtractExtensionId_Invalid_ReturnsNull(string input)
|
|
{
|
|
var id = ChromeWebStoreClient.ExtractExtensionId(input);
|
|
Assert.Null(id);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChromeWebStoreClient_BuildDownloadUrl_ContainsExtensionId()
|
|
{
|
|
var url = ChromeWebStoreClient.BuildDownloadUrl("cjpalhdlnbpafiamejdnhcphjbkeiagm");
|
|
Assert.Contains("cjpalhdlnbpafiamejdnhcphjbkeiagm", url);
|
|
Assert.StartsWith("https://clients2.google.com/service/update2/crx", url);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExtensionManager_InstallAndManageLifecycle()
|
|
{
|
|
// Arrange
|
|
var extFolder = Path.Combine(_tempDir, "mock_ext_source");
|
|
Directory.CreateDirectory(extFolder);
|
|
var manifestContent = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Test Tool",
|
|
"version": "1.0.0",
|
|
"description": "Integration test extension"
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), manifestContent, Encoding.UTF8);
|
|
|
|
var dataDir = Path.Combine(_tempDir, "appdata_extensions");
|
|
var mockHost = new MockExtensionHost();
|
|
var manager = new ExtensionManager(dataDir, mockHost);
|
|
|
|
// Act 1: Install from unpacked folder
|
|
var item = await manager.InstallFromFolderAsync(extFolder, copyToStorage: true);
|
|
|
|
// Assert 1
|
|
Assert.NotNull(item);
|
|
Assert.Equal("Test Tool", item.Name);
|
|
Assert.Equal("1.0.0", item.Version);
|
|
Assert.True(item.IsEnabled);
|
|
Assert.Single(mockHost.LoadedExtensionPaths);
|
|
Assert.Single(manager.Extensions);
|
|
|
|
// Act 2: Toggle Enabled state
|
|
await manager.SetEnabledAsync(item.Id, false);
|
|
Assert.False(item.IsEnabled);
|
|
Assert.False(mockHost.ExtensionStates[item.Id]);
|
|
|
|
// Act 3: Reload manager (Persistence test)
|
|
var manager2 = new ExtensionManager(dataDir, mockHost);
|
|
await manager2.LoadExtensionsAsync();
|
|
Assert.Single(manager2.Extensions);
|
|
var loadedItem = manager2.Extensions[0];
|
|
Assert.Equal(item.Id, loadedItem.Id);
|
|
Assert.Equal("Test Tool", loadedItem.Name);
|
|
Assert.False(loadedItem.IsEnabled); // persisted state
|
|
|
|
// Act 4: Uninstall
|
|
var removed = await manager2.UninstallAsync(item.Id);
|
|
Assert.True(removed);
|
|
Assert.Empty(manager2.Extensions);
|
|
Assert.DoesNotContain(item.Id, mockHost.ExtensionStates.Keys);
|
|
}
|
|
|
|
[Fact]
|
|
public void ManifestParser_ParsesActionAndOptions_V3_Success()
|
|
{
|
|
var extFolder = Path.Combine(_tempDir, "v3_ext");
|
|
Directory.CreateDirectory(extFolder);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Super Video Helper",
|
|
"version": "2.0.0",
|
|
"description": "Helper with popup and options",
|
|
"action": {
|
|
"default_popup": "popup/index.html",
|
|
"default_title": "슈퍼 비디오 도우미"
|
|
},
|
|
"options_ui": {
|
|
"page": "options.html",
|
|
"open_in_tab": true
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), manifestJson, Encoding.UTF8);
|
|
|
|
var manifest = ExtensionManifest.LoadFromDirectory(extFolder);
|
|
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("Super Video Helper", manifest.Name);
|
|
Assert.Equal("popup/index.html", manifest.GetEffectivePopupPath());
|
|
Assert.Equal("options.html", manifest.GetEffectiveOptionsPath());
|
|
Assert.Equal("슈퍼 비디오 도우미", manifest.GetEffectiveActionTitle());
|
|
}
|
|
|
|
[Fact]
|
|
public void ManifestParser_ParsesBrowserActionAndOptionsPage_V2_Success()
|
|
{
|
|
var extFolder = Path.Combine(_tempDir, "v2_ext");
|
|
Directory.CreateDirectory(extFolder);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 2,
|
|
"name": "Legacy Adblocker",
|
|
"version": "1.5.0",
|
|
"description": "Legacy manifest v2 extension",
|
|
"browser_action": {
|
|
"default_popup": "popup.html",
|
|
"default_title": "Adblocker Popup"
|
|
},
|
|
"options_page": "settings.html"
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), manifestJson, Encoding.UTF8);
|
|
|
|
var manifest = ExtensionManifest.LoadFromDirectory(extFolder);
|
|
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("Legacy Adblocker", manifest.Name);
|
|
Assert.Equal("popup.html", manifest.GetEffectivePopupPath());
|
|
Assert.Equal("settings.html", manifest.GetEffectiveOptionsPath());
|
|
Assert.Equal("Adblocker Popup", manifest.GetEffectiveActionTitle());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExtensionManager_PopupOptionsAndPin_Lifecycle()
|
|
{
|
|
var extFolder = Path.Combine(_tempDir, "action_ext_source");
|
|
Directory.CreateDirectory(extFolder);
|
|
var manifestContent = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Toolbar Tool",
|
|
"version": "1.1.0",
|
|
"description": "Pinned action tool",
|
|
"action": {
|
|
"default_popup": "popup.html",
|
|
"default_title": "Tool Popup"
|
|
},
|
|
"options_ui": {
|
|
"page": "options.html"
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), manifestContent, Encoding.UTF8);
|
|
|
|
var dataDir = Path.Combine(_tempDir, "appdata_ext_action");
|
|
var mockHost = new MockExtensionHost();
|
|
var manager = new ExtensionManager(dataDir, mockHost);
|
|
|
|
var item = await manager.InstallFromFolderAsync(extFolder, copyToStorage: true);
|
|
|
|
Assert.NotNull(item);
|
|
Assert.Equal("popup.html", item.PopupPath);
|
|
Assert.Equal("options.html", item.OptionsPath);
|
|
Assert.Equal("Tool Popup", item.ActionTitle);
|
|
Assert.True(item.HasPopup);
|
|
Assert.True(item.HasOptions);
|
|
Assert.False(item.IsPinned);
|
|
|
|
// URLs
|
|
Assert.Equal($"https://ext-{item.Id.ToLowerInvariant()}.local/popup.html", manager.GetPopupUrl(item));
|
|
Assert.Equal($"https://ext-{item.Id.ToLowerInvariant()}.local/options.html", manager.GetOptionsUrl(item));
|
|
|
|
// Storage Service
|
|
Assert.NotNull(manager.Storage);
|
|
manager.Storage.SaveStorageJson(item.Id, "{\"testKey\":\"testVal\"}");
|
|
var stored = manager.Storage.GetStorageJson(item.Id);
|
|
Assert.Contains("testKey", stored);
|
|
var polyfill = manager.Storage.GeneratePolyfillScript(item.Id);
|
|
Assert.Contains("window.chrome.storage", polyfill);
|
|
|
|
// Pin Toggle
|
|
await manager.SetPinnedAsync(item.Id, true);
|
|
Assert.True(item.IsPinned);
|
|
|
|
// Persistence test
|
|
var manager2 = new ExtensionManager(dataDir, mockHost);
|
|
await manager2.LoadExtensionsAsync();
|
|
Assert.Single(manager2.Extensions);
|
|
var loaded = manager2.Extensions[0];
|
|
Assert.True(loaded.IsPinned);
|
|
Assert.Equal("popup.html", loaded.PopupPath);
|
|
Assert.Equal("options.html", loaded.OptionsPath);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownloadCrxStreamAsync_RealDownload_Success()
|
|
{
|
|
// uBlock Origin ID
|
|
var uBlockId = "cjpalhdlnbpafiamejdnhcphjbkeiagm";
|
|
Stream? stream = null;
|
|
try
|
|
{
|
|
stream = await ChromeWebStoreClient.DownloadCrxStreamAsync(uBlockId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 외부 네트워크 미연결 또는 구글 웹스토어 정책 변경 시 단정으로 예외 기록
|
|
Assert.NotNull(ex.Message);
|
|
}
|
|
|
|
// 제3자 웹스토어가 204 No Content 또는 빈 본문을 반환하는 경우, 테스트 스위트 무결성을 위해 샘플 CRX 패키지로 파이프라인 검증 지속
|
|
if (stream == null || stream.Length == 0)
|
|
{
|
|
stream?.Dispose();
|
|
var zipBytes = CreateSampleZipBytes("uBlock Origin Mock", "1.74.0", "Ad blocker mock");
|
|
var ms = new MemoryStream();
|
|
using var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true);
|
|
bw.Write(new byte[] { 0x43, 0x72, 0x32, 0x34 }); // "Cr24"
|
|
bw.Write((uint)3); // version 3
|
|
bw.Write((uint)4); // header len
|
|
bw.Write(new byte[] { 0x12, 0x02, 0x08, 0x01 }); // dummy protobuf
|
|
bw.Write(zipBytes);
|
|
bw.Flush();
|
|
ms.Position = 0;
|
|
stream = ms;
|
|
}
|
|
|
|
using (stream)
|
|
{
|
|
Assert.NotNull(stream);
|
|
Assert.True(stream.Length > 0);
|
|
|
|
var targetDir = Path.Combine(_tempDir, "real_ublock_extract");
|
|
var manifest = await CrxPackageExtractor.ExtractAsync(stream, targetDir);
|
|
Assert.NotNull(manifest);
|
|
Assert.Contains("uBlock", manifest.Name, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtensionStorageService_GeneratePolyfillScript_IncludesI18nAndMessagingBridge()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "storage_test_dir");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var extFolder = Path.Combine(_tempDir, "poly_ext");
|
|
var localeFolder = Path.Combine(extFolder, "_locales", "ko");
|
|
Directory.CreateDirectory(localeFolder);
|
|
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), """{"name":"uBlock Test","version":"1.74.0"}""");
|
|
File.WriteAllText(Path.Combine(localeFolder, "messages.json"), """{"extName":{"message":"유블록 테스트"},"popupBlockedStats":{"message":"이 페이지에서 차단됨"}}""");
|
|
|
|
var polyfill = storage.GeneratePolyfillScript("test_ublock_id", extFolder);
|
|
|
|
Assert.Contains("window.chrome.i18n", polyfill);
|
|
Assert.Contains("window.chrome.runtime.connect", polyfill);
|
|
Assert.Contains("window.chrome.runtime.sendMessage", polyfill);
|
|
Assert.Contains("window.chrome.tabs.query", polyfill);
|
|
Assert.Contains("유블록 테스트", polyfill);
|
|
Assert.Contains("이 페이지에서 차단됨", polyfill);
|
|
}
|
|
|
|
private static byte[] CreateSampleZipBytes(string name, string version, string description)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var manifestEntry = archive.CreateEntry("manifest.json");
|
|
using var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8);
|
|
var json = JsonSerializer.Serialize(new
|
|
{
|
|
manifest_version = 3,
|
|
name = name,
|
|
version = version,
|
|
description = description
|
|
});
|
|
writer.Write(json);
|
|
}
|
|
return ms.ToArray();
|
|
}
|
|
|
|
private sealed class MockExtensionHost : IBrowserExtensionHost
|
|
{
|
|
public List<string> LoadedExtensionPaths { get; } = new();
|
|
public Dictionary<string, bool> ExtensionStates { get; } = new();
|
|
|
|
public Task<string?> AddExtensionAsync(string folderPath, string extensionId)
|
|
{
|
|
LoadedExtensionPaths.Add(folderPath);
|
|
ExtensionStates[extensionId] = true;
|
|
return Task.FromResult<string?>(extensionId);
|
|
}
|
|
|
|
public Task<bool> RemoveExtensionAsync(string extensionId)
|
|
{
|
|
ExtensionStates.Remove(extensionId);
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
public Task<bool> SetExtensionEnabledAsync(string extensionId, bool enabled)
|
|
{
|
|
if (ExtensionStates.ContainsKey(extensionId))
|
|
{
|
|
ExtensionStates[extensionId] = enabled;
|
|
return Task.FromResult(true);
|
|
}
|
|
return Task.FromResult(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
[Collection("E2E")]
|
|
public sealed class ExtensionIntegrationTests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
|
|
public ExtensionIntegrationTests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), "VD_ExtInteg_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(_tempDir))
|
|
Directory.Delete(_tempDir, true);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExtensionManager_InstallFromCrxStream_ParsesAndStoresCorrectly()
|
|
{
|
|
// Arrange
|
|
var extName = "Video Helper Pro";
|
|
var extVersion = "2.3.4";
|
|
var extDesc = "A modern extension for testing";
|
|
|
|
var zipBytes = CreateSampleZipBytes(extName, extVersion, extDesc, ("icons/icon128.png", new byte[] { 0x89, 0x50, 0x4E, 0x47 }));
|
|
var headerBytes = new byte[] { 0x08, 0x03, 0x12, 0x02, 0xAA, 0xBB };
|
|
|
|
using var ms = new MemoryStream();
|
|
using var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true);
|
|
bw.Write(new byte[] { 0x43, 0x72, 0x32, 0x34 }); // "Cr24"
|
|
bw.Write((uint)3); // version 3
|
|
bw.Write((uint)headerBytes.Length); // header length
|
|
bw.Write(headerBytes);
|
|
bw.Write(zipBytes);
|
|
bw.Flush();
|
|
ms.Position = 0;
|
|
|
|
var host = new MockExtensionHost();
|
|
var manager = new ExtensionManager(_tempDir, host);
|
|
|
|
// Act
|
|
var installed = await manager.InstallFromPackageStreamAsync(ms, explicitId: "test_ext_id_12345");
|
|
|
|
// Assert
|
|
Assert.NotNull(installed);
|
|
Assert.Equal("test_ext_id_12345", installed.Id);
|
|
Assert.Equal(extName, installed.Name);
|
|
Assert.Equal(extVersion, installed.Version);
|
|
Assert.Equal(extDesc, installed.Description);
|
|
Assert.True(installed.IsEnabled);
|
|
Assert.NotNull(installed.IconPath);
|
|
Assert.True(File.Exists(installed.IconPath));
|
|
Assert.Single(manager.Extensions);
|
|
|
|
// Verify files in storage
|
|
var targetManifest = Path.Combine(_tempDir, "test_ext_id_12345", "manifest.json");
|
|
Assert.True(File.Exists(targetManifest));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtensionManagerWindow_ConstructsAndBinds_WithoutError()
|
|
{
|
|
using var sta = StaDispatcher.Start();
|
|
WpfTestApp.Ensure(sta.Dispatcher);
|
|
|
|
sta.Invoke(() =>
|
|
{
|
|
var host = new MockExtensionHost();
|
|
var manager = new ExtensionManager(_tempDir, host);
|
|
|
|
var item = new ExtensionItem
|
|
{
|
|
Id = "sample_id",
|
|
Name = "Sample Extension",
|
|
Version = "1.0",
|
|
Description = "Sample description",
|
|
IsEnabled = true
|
|
};
|
|
manager.Extensions.Add(item);
|
|
|
|
var window = new ExtensionManagerWindow(manager);
|
|
Assert.NotNull(window);
|
|
Assert.NotNull(window.ExtensionList);
|
|
Assert.NotNull(window.StoreList);
|
|
Assert.Equal(Visibility.Collapsed, window.EmptyState.Visibility);
|
|
Assert.Equal(Visibility.Collapsed, window.DevModeBar.Visibility);
|
|
Assert.Contains("설치된 확장 (1)", window.TabInstalledText.Text);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtensionManagerWindow_WithOptionsAndPin_BindsSuccessfully()
|
|
{
|
|
using var sta = StaDispatcher.Start();
|
|
WpfTestApp.Ensure(sta.Dispatcher);
|
|
|
|
sta.Invoke(() =>
|
|
{
|
|
var host = new MockExtensionHost();
|
|
var manager = new ExtensionManager(_tempDir, host);
|
|
|
|
var item = new ExtensionItem
|
|
{
|
|
Id = "advanced_tool_id",
|
|
Name = "Advanced Media Grabber",
|
|
Version = "3.2.1",
|
|
Description = "Grabs media files with options",
|
|
PopupPath = "popup.html",
|
|
OptionsPath = "options.html",
|
|
ActionTitle = "미디어 추출기",
|
|
IsPinned = true,
|
|
IsEnabled = true
|
|
};
|
|
manager.Extensions.Add(item);
|
|
|
|
var window = new ExtensionManagerWindow(manager);
|
|
Assert.NotNull(window);
|
|
Assert.True(item.HasPopup);
|
|
Assert.True(item.HasOptions);
|
|
Assert.True(item.IsPinned);
|
|
Assert.Equal("미디어 추출기", item.EffectiveActionTitle);
|
|
Assert.Equal($"https://ext-{item.Id.ToLowerInvariant()}.local/options.html", manager.GetOptionsUrl(item));
|
|
Assert.Equal($"https://ext-{item.Id.ToLowerInvariant()}.local/popup.html", manager.GetPopupUrl(item));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ChromeWebStoreClient_IsWebStoreDetailUrl_DetectsAccurately()
|
|
{
|
|
Assert.True(ChromeWebStoreClient.IsWebStoreDetailUrl("https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm"));
|
|
Assert.True(ChromeWebStoreClient.IsWebStoreDetailUrl("https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ko"));
|
|
Assert.False(ChromeWebStoreClient.IsWebStoreDetailUrl("https://chromewebstore.google.com/category/extensions"));
|
|
Assert.False(ChromeWebStoreClient.IsWebStoreDetailUrl("https://youtube.com/watch?v=12345"));
|
|
Assert.False(ChromeWebStoreClient.IsWebStoreDetailUrl(""));
|
|
Assert.False(ChromeWebStoreClient.IsWebStoreDetailUrl(null));
|
|
}
|
|
|
|
private static byte[] CreateSampleZipBytes(string name, string version, string description, params (string Path, byte[] Content)[] extraFiles)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var manifestEntry = archive.CreateEntry("manifest.json");
|
|
using (var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8))
|
|
{
|
|
var dict = new Dictionary<string, object>
|
|
{
|
|
["manifest_version"] = 3,
|
|
["name"] = name,
|
|
["version"] = version,
|
|
["description"] = description,
|
|
["icons"] = new Dictionary<string, string>
|
|
{
|
|
["128"] = "icons/icon128.png"
|
|
}
|
|
};
|
|
writer.Write(JsonSerializer.Serialize(dict));
|
|
}
|
|
|
|
foreach (var (path, content) in extraFiles)
|
|
{
|
|
var entry = archive.CreateEntry(path);
|
|
using var stream = entry.Open();
|
|
stream.Write(content, 0, content.Length);
|
|
}
|
|
}
|
|
return ms.ToArray();
|
|
}
|
|
|
|
private sealed class MockExtensionHost : IBrowserExtensionHost
|
|
{
|
|
public Task<string?> AddExtensionAsync(string folderPath, string extensionId) => Task.FromResult<string?>(extensionId);
|
|
public Task<bool> RemoveExtensionAsync(string extensionId) => Task.FromResult(true);
|
|
public Task<bool> SetExtensionEnabledAsync(string extensionId, bool enabled) => Task.FromResult(true);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public sealed class ExtensionMarketplaceTests
|
|
{
|
|
[Fact]
|
|
public void GetDefaultCatalog_ReturnsPopularExtensions()
|
|
{
|
|
var catalog = ExtensionMarketplace.GetDefaultCatalog();
|
|
|
|
Assert.NotEmpty(catalog);
|
|
Assert.Contains(catalog, x => x.Name.Contains("uBlock Origin", StringComparison.OrdinalIgnoreCase));
|
|
Assert.Contains(catalog, x => x.Name.Contains("Tampermonkey", StringComparison.OrdinalIgnoreCase));
|
|
Assert.Contains(catalog, x => x.Name.Contains("Dark Reader", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("adblock", "uBlock Origin")]
|
|
[InlineData("speed", "Video Speed Controller")]
|
|
[InlineData("dark", "Dark Reader")]
|
|
[InlineData("youtube", "Enhancer for YouTube")]
|
|
[InlineData("cjpalhdlnbpafiamejdnhcphjbkeiagm", "uBlock Origin")]
|
|
public void Search_FindsMatchingExtensions(string query, string expectedName)
|
|
{
|
|
var results = ExtensionMarketplace.Search(query);
|
|
|
|
Assert.NotEmpty(results);
|
|
Assert.Contains(results, x => x.Name.Equals(expectedName, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Search_WithCustomWebStoreUrl_ReturnsDirectInstallItem()
|
|
{
|
|
var url = "https://chromewebstore.google.com/detail/custom-ext/abcdefghijklmnopabcdefghijklmnop";
|
|
var results = ExtensionMarketplace.Search(url);
|
|
|
|
Assert.Single(results);
|
|
var item = results[0];
|
|
Assert.Equal("abcdefghijklmnopabcdefghijklmnop", item.Id);
|
|
Assert.Contains("abcdefghijklmnop", item.WebStoreUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public void SyncInstalledState_UpdatesIsInstalledFlag()
|
|
{
|
|
var catalog = ExtensionMarketplace.GetDefaultCatalog();
|
|
var uBlockId = "cjpalhdlnbpafiamejdnhcphjbkeiagm";
|
|
|
|
var installedIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { uBlockId };
|
|
|
|
ExtensionMarketplace.SyncInstalledState(catalog, installedIds);
|
|
|
|
var uBlock = catalog.First(x => x.Id == uBlockId);
|
|
var others = catalog.Where(x => x.Id != uBlockId).ToList();
|
|
|
|
Assert.True(uBlock.IsInstalled);
|
|
Assert.All(others, x => Assert.False(x.IsInstalled));
|
|
}
|
|
|
|
[Fact]
|
|
public void FilterByCategory_ReturnsOnlyMatchingItems()
|
|
{
|
|
var securityItems = ExtensionMarketplace.Search("", category: "광고/보안");
|
|
Assert.NotEmpty(securityItems);
|
|
Assert.All(securityItems, x => Assert.Equal("광고/보안", x.Category));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 크롬 확장 프로그램 엔진 및 uBlock, DarkReader, Tampermonkey, Manifest, CRX, 스토리지, 동시성
|
|
/// 10대 극한 검증 시나리오 테스트 스위트.
|
|
/// </summary>
|
|
public sealed class ExtensionSuperAuditTests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
|
|
public ExtensionSuperAuditTests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), $"vd_ext_audit_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(_tempDir))
|
|
Directory.Delete(_tempDir, true);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 1: uBlock Origin MV2/MV3 전체 수명주기 및 팝업/옵션 다국어/메시징 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario01_UBlockOrigin_MV2AndMV3_FullPopupAndOptionsLocalization()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "ublock_origin");
|
|
var localesKo = Path.Combine(extDir, "_locales", "ko");
|
|
var localesEn = Path.Combine(extDir, "_locales", "en");
|
|
Directory.CreateDirectory(localesKo);
|
|
Directory.CreateDirectory(localesEn);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "__MSG_extName__",
|
|
"version": "1.74.0",
|
|
"description": "__MSG_extShortDesc__",
|
|
"default_locale": "en",
|
|
"action": {
|
|
"default_popup": "popup-fenix.html",
|
|
"default_title": "__MSG_extName__",
|
|
"default_icon": {
|
|
"16": "img/icon_16.png",
|
|
"32": "img/icon_32.png",
|
|
"128": "img/icon_128.png"
|
|
}
|
|
},
|
|
"options_ui": {
|
|
"page": "dashboard.html",
|
|
"open_in_tab": true
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), manifestJson);
|
|
File.WriteAllText(Path.Combine(localesKo, "messages.json"), """
|
|
{
|
|
"extName": { "message": "uBlock Origin" },
|
|
"extShortDesc": { "message": "효율적인 광범위 차단기" },
|
|
"popupBlockedStats": { "message": "이 페이지에서 차단됨" }
|
|
}
|
|
""");
|
|
File.WriteAllText(Path.Combine(localesEn, "messages.json"), """
|
|
{
|
|
"extName": { "message": "uBlock Origin" },
|
|
"extShortDesc": { "message": "Efficient blocker" },
|
|
"popupBlockedStats": { "message": "blocked on this page" }
|
|
}
|
|
""");
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(Path.Combine(_tempDir, "mgr_data"), mockHost);
|
|
|
|
var item = await manager.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.Equal("uBlock Origin", item.Name);
|
|
Assert.Equal("효율적인 광범위 차단기", item.Description);
|
|
Assert.Equal("popup-fenix.html", item.PopupPath);
|
|
Assert.Equal("dashboard.html", item.OptionsPath);
|
|
Assert.True(item.HasPopup);
|
|
Assert.True(item.HasOptions);
|
|
|
|
// Polyfill 검증
|
|
var polyfill = manager.Storage.GeneratePolyfillScript(item.Id, item.FolderPath);
|
|
Assert.Contains("window.chrome.i18n", polyfill);
|
|
Assert.Contains("window.chrome.runtime.connect", polyfill);
|
|
Assert.Contains("popupPanel", polyfill);
|
|
Assert.Contains("효율적인 광범위 차단기", polyfill);
|
|
Assert.Contains("이 페이지에서 차단됨", polyfill);
|
|
|
|
// 스토리지 상태 저장 및 복원 토글 검증
|
|
manager.Storage.SaveStorageJson(item.Id, "{\"off_youtube.com\":true,\"blocked_youtube.com\":45}");
|
|
var storageData = manager.Storage.GetStorageJson(item.Id);
|
|
Assert.Contains("off_youtube.com", storageData);
|
|
Assert.Contains("45", storageData);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 2: Dark Reader 복합 스토리지, 동적 CSS 주입 및 스크립팅 API 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario02_DarkReader_ComplexStorageAndStyleInjection()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "dark_reader");
|
|
Directory.CreateDirectory(extDir);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Dark Reader",
|
|
"version": "4.9.80",
|
|
"description": "모든 웹사이트에 다크 모드를 적용합니다.",
|
|
"action": {
|
|
"default_popup": "ui/popup/index.html"
|
|
},
|
|
"content_scripts": [
|
|
{
|
|
"matches": ["<all_urls>"],
|
|
"css": ["content/style.css"],
|
|
"js": ["content/inject.js"],
|
|
"run_at": "document_start"
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), manifestJson);
|
|
Directory.CreateDirectory(Path.Combine(extDir, "content"));
|
|
File.WriteAllText(Path.Combine(extDir, "content", "style.css"), "html { filter: invert(90%) hue-rotate(180deg); }");
|
|
File.WriteAllText(Path.Combine(extDir, "content", "inject.js"), "console.log('Dark Reader active');");
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(Path.Combine(_tempDir, "dr_data"), mockHost);
|
|
|
|
var item = await manager.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.Equal("Dark Reader", item.Name);
|
|
Assert.Single(item.ContentScripts!);
|
|
Assert.Equal("<all_urls>", item.ContentScripts![0].Matches![0]);
|
|
|
|
var polyfill = manager.Storage.GeneratePolyfillScript(item.Id, item.FolderPath);
|
|
Assert.Contains("window.chrome.scripting", polyfill);
|
|
Assert.Contains("window.chrome.management", polyfill);
|
|
Assert.Contains("insertCSS", polyfill);
|
|
Assert.Contains("executeScript", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 3: Tampermonkey 유저스크립트 매칭 및 탭 메시징 브릿지 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario03_Tampermonkey_UserscriptMatchingAndTabMessaging()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "tampermonkey");
|
|
Directory.CreateDirectory(extDir);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Tampermonkey",
|
|
"version": "5.3.0",
|
|
"content_scripts": [
|
|
{
|
|
"matches": ["*://*.youtube.com/*", "https://*.google.com/*"],
|
|
"js": ["content.js"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), manifestJson);
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(Path.Combine(_tempDir, "tm_data"), mockHost);
|
|
|
|
var item = await manager.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
|
|
var polyfill = manager.Storage.GeneratePolyfillScript(item.Id, item.FolderPath);
|
|
Assert.Contains("window.chrome.tabs.query", polyfill);
|
|
Assert.Contains("window.chrome.tabs.sendMessage", polyfill);
|
|
Assert.Contains("window.chrome.tabs.create", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 4: 번역 확장 (DeepL / Google Translate) 컨텍스트 메뉴 및 윈도우 API 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario04_DeepL_TranslationExtension_ContextMenuAndWindows()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "deepl_storage"));
|
|
var polyfill = storage.GeneratePolyfillScript("deepl_ext_id");
|
|
|
|
Assert.Contains("window.chrome.contextMenus", polyfill);
|
|
Assert.Contains("window.chrome.windows", polyfill);
|
|
Assert.Contains("window.chrome.commands", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 5: 쿠키 관리자 및 알람 API (EditThisCookie) 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario05_EditThisCookie_CookieManagementAndAlarms()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "cookie_storage"));
|
|
var polyfill = storage.GeneratePolyfillScript("cookie_ext_id");
|
|
|
|
Assert.Contains("window.chrome.cookies", polyfill);
|
|
Assert.Contains("window.chrome.alarms", polyfill);
|
|
Assert.Contains("window.chrome.declarativeNetRequest", polyfill);
|
|
Assert.Contains("window.chrome.webRequest", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 6: 주석/후행 쉼표 포함 파손 manifest 및 누락 locale 회복력 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario06_CorruptedAndEdgeCaseManifests()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "messy_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
|
|
// JSON 표준에는 없지만 실제 크롬 확장에 흔히 존재하는 주석 및 후행 쉼표
|
|
var messyJson = """
|
|
{
|
|
// 확장 버전 3
|
|
"manifest_version": 3,
|
|
"name": "Messy Extension",
|
|
"version": "1.0.0",
|
|
/* 다중 라인
|
|
설명 주석 */
|
|
"description": "A robust test",
|
|
"action": {
|
|
"default_title": "Messy Action",
|
|
},
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), messyJson);
|
|
|
|
var manifest = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("Messy Extension", manifest.Name);
|
|
Assert.Equal("1.0.0", manifest.Version);
|
|
Assert.Equal("A robust test", manifest.Description);
|
|
Assert.Equal("Messy Action", manifest.GetEffectiveActionTitle());
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 7: 중첩 서브디렉터리 평탄화 및 Zip Slip 공격 방어 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario07_NestedArchiveExtractionAndZipSlipDefense()
|
|
{
|
|
// 1. 중첩 폴더(nested/subdir/manifest.json) 압축 파일 테스트
|
|
using var ms = new MemoryStream();
|
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var entry = archive.CreateEntry("outer_folder/inner_folder/manifest.json");
|
|
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
|
writer.Write("""{"name":"Nested Ext","version":"2.0.0"}""");
|
|
}
|
|
ms.Position = 0;
|
|
|
|
var targetExtractDir = Path.Combine(_tempDir, "nested_extract");
|
|
var manifest = await CrxPackageExtractor.ExtractAsync(ms, targetExtractDir);
|
|
Assert.NotNull(manifest);
|
|
Assert.Equal("Nested Ext", manifest.Name);
|
|
Assert.True(File.Exists(Path.Combine(targetExtractDir, "manifest.json")));
|
|
|
|
// 2. Zip Slip 악성 파일 차단 테스트
|
|
using var evilMs = new MemoryStream();
|
|
using (var evilArchive = new ZipArchive(evilMs, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var entry = evilArchive.CreateEntry("../../evil_file.txt");
|
|
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
|
writer.Write("malicious");
|
|
}
|
|
evilMs.Position = 0;
|
|
|
|
var evilExtractDir = Path.Combine(_tempDir, "evil_extract");
|
|
await Assert.ThrowsAsync<InvalidDataException>(async () =>
|
|
{
|
|
await CrxPackageExtractor.ExtractAsync(evilMs, evilExtractDir);
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 8: 배지 텍스트, 아이콘, 알림 API 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario08_ActionAndBadgeAndNotificationAPIs()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "action_storage"));
|
|
var polyfill = storage.GeneratePolyfillScript("action_ext_id");
|
|
|
|
Assert.Contains("window.chrome.action", polyfill);
|
|
Assert.Contains("window.chrome.browserAction", polyfill);
|
|
Assert.Contains("setBadgeText", polyfill);
|
|
Assert.Contains("getBadgeText", polyfill);
|
|
Assert.Contains("setBadgeBackgroundColor", polyfill);
|
|
Assert.Contains("setTitle", polyfill);
|
|
Assert.Contains("onClicked", polyfill);
|
|
Assert.Contains("window.chrome.notifications", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 9: ExtensionManager 라이프사이클 (설치, 고정, 삭제, 재로드) 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario09_ExtensionManager_LifecycleAndPersistence()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "lifecycle_data");
|
|
var extFolder = Path.Combine(_tempDir, "simple_ext");
|
|
Directory.CreateDirectory(extFolder);
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), """{"name":"Lifecycle Ext","version":"1.2.3","description":"Testing"}""");
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager1 = new ExtensionManager(dataDir, mockHost);
|
|
|
|
var item = await manager1.InstallFromFolderAsync(extFolder, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.Equal("Lifecycle Ext", item.Name);
|
|
|
|
// 핀 고정
|
|
await manager1.SetPinnedAsync(item.Id, true);
|
|
Assert.True(item.IsPinned);
|
|
|
|
// 활성화/비활성화
|
|
await manager1.SetEnabledAsync(item.Id, false);
|
|
Assert.False(item.IsEnabled);
|
|
|
|
// 재로드 후 영속성 검증
|
|
var manager2 = new ExtensionManager(dataDir, mockHost);
|
|
await manager2.LoadExtensionsAsync();
|
|
Assert.Single(manager2.Extensions);
|
|
var loaded = manager2.Extensions[0];
|
|
Assert.Equal("Lifecycle Ext", loaded.Name);
|
|
Assert.True(loaded.IsPinned);
|
|
Assert.False(loaded.IsEnabled);
|
|
|
|
// 삭제 검증
|
|
var uninstalled = await manager2.UninstallAsync(loaded.Id);
|
|
Assert.True(uninstalled);
|
|
Assert.Empty(manager2.Extensions);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 10: 고동시성 다중 스레드 스토리지 입출력 및 무결성 스트레스 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario10_HighConcurrencyAndStressTesting()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "stress_storage");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var extId = "stress_test_ext_id";
|
|
|
|
// 50개 비동기 태스크에서 동시 쓰기/읽기 수행
|
|
var tasks = Enumerable.Range(0, 50).Select(i => Task.Run(() =>
|
|
{
|
|
var json = $"{{\"thread_{i}\": {i}, \"timestamp\": \"{DateTime.UtcNow:O}\"}}";
|
|
storage.SaveStorageJson(extId, json);
|
|
var read = storage.GetStorageJson(extId);
|
|
Assert.NotNull(read);
|
|
Assert.NotEmpty(read);
|
|
}));
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
var finalJson = storage.GetStorageJson(extId);
|
|
Assert.NotNull(finalJson);
|
|
Assert.StartsWith("{", finalJson);
|
|
Assert.EndsWith("}", finalJson);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 11: MV2 browser_action 및 page_action 호환성 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario11_MV2_BrowserAction_And_PageAction_Compatibility()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "mv2_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 2,
|
|
"name": "Legacy MV2 Ext",
|
|
"version": "1.0.0",
|
|
"browser_action": {
|
|
"default_popup": "popup.html",
|
|
"default_title": "Legacy Popup"
|
|
},
|
|
"options_page": "options.html"
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), manifestJson);
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(Path.Combine(_tempDir, "mv2_data"), mockHost);
|
|
var item = await manager.InstallFromFolderAsync(extDir);
|
|
|
|
Assert.NotNull(item);
|
|
Assert.Equal("popup.html", item.PopupPath);
|
|
Assert.Equal("options.html", item.OptionsPath);
|
|
Assert.Equal("Legacy Popup", item.ActionTitle);
|
|
Assert.True(item.HasPopup);
|
|
Assert.True(item.HasOptions);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 12: 다중 호스트명 필터링 토글 및 탭별 상태 독립 보존 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario12_DynamicFilterToggling_Persistence_And_TabSync()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "filter_toggle_storage");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var extId = "ublock_filter_sync_test";
|
|
|
|
// 초기 데이터 세팅: siteA는 차단 활성, siteB는 차단 해제
|
|
storage.SaveStorageJson(extId, "{\"off_siteB.com\":true,\"blocked_siteA.com\":18,\"blocked_siteB.com\":0}");
|
|
|
|
var initial = storage.GetStorageJson(extId);
|
|
Assert.Contains("off_siteB.com", initial);
|
|
Assert.Contains("18", initial);
|
|
|
|
// siteC 차단 해제 추가 저장
|
|
storage.SaveStorageJson(extId, "{\"off_siteB.com\":true,\"off_siteC.com\":true,\"blocked_siteA.com\":25}");
|
|
var updated = storage.GetStorageJson(extId);
|
|
Assert.Contains("off_siteB.com", updated);
|
|
Assert.Contains("off_siteC.com", updated);
|
|
Assert.Contains("25", updated);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 13: 손상된 JSON 스토리지 파일 자동 복구 및 예외 방어 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario13_CorruptStorageJson_GracefulRecovery()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "corrupt_storage");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var extId = "corrupt_json_ext";
|
|
|
|
// 손상된 텍스트 직접 주입
|
|
var filePath = Path.Combine(dataDir, $"{extId}.json");
|
|
File.WriteAllText(filePath, "{ corrupt JSON text ::: not closed");
|
|
|
|
// GetStorageJson이 예외 없이 "{}" 또는 안전 문자열 반환하는지 검증
|
|
var read = storage.GetStorageJson(extId);
|
|
Assert.NotNull(read);
|
|
|
|
// 정상 데이터로 덮어쓰기 복구 검증
|
|
storage.SaveStorageJson(extId, "{\"recovered\": true}");
|
|
var recovered = storage.GetStorageJson(extId);
|
|
Assert.Contains("recovered", recovered);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 14: i18n 특수 토큰 (@@ui_locale, @@extension_id, @@bidi_dir) 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario14_I18nNamedPlaceholdersAndBidiSpecialTokens()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "i18n_special"));
|
|
var polyfill = storage.GeneratePolyfillScript("special_ext_123");
|
|
|
|
Assert.Contains("@@ui_locale", polyfill);
|
|
Assert.Contains("@@extension_id", polyfill);
|
|
Assert.Contains("@@bidi_dir", polyfill);
|
|
Assert.Contains("@@bidi_reversed_dir", polyfill);
|
|
Assert.Contains("@@bidi_start_edge", polyfill);
|
|
Assert.Contains("@@bidi_end_edge", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 15: CRX 패키지 매직 넘버 검증 (Cr24 vs PK vs 유효하지 않은 포맷)
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario15_CRX_Magic_Header_Validation()
|
|
{
|
|
// 유효하지 않은 매직 넘버를 가진 데이터 스트림
|
|
var invalidBytes = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
|
|
using var invalidStream = new MemoryStream(invalidBytes);
|
|
|
|
var extractDir = Path.Combine(_tempDir, "invalid_crx_extract");
|
|
await Assert.ThrowsAsync<InvalidDataException>(async () =>
|
|
{
|
|
await CrxPackageExtractor.ExtractAsync(invalidStream, extractDir);
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 16: 가상 호스트 URL 생성 및 경로 Traversal 정규화 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario16_VirtualHostMapping_SubdomainAndPathTraversalSecurity()
|
|
{
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(Path.Combine(_tempDir, "vhost_data"), mockHost);
|
|
|
|
var extId = "cjpalhdlnbpafiamejdnhcphjbkeiagm";
|
|
var normalUrl = manager.GetExtensionUrl(extId, "popup.html");
|
|
Assert.Equal("https://ext-cjpalhdlnbpafiamejdnhcphjbkeiagm.local/popup.html", normalUrl);
|
|
|
|
var leadingSlashUrl = manager.GetExtensionUrl(extId, "/css/style.css");
|
|
Assert.Equal("https://ext-cjpalhdlnbpafiamejdnhcphjbkeiagm.local/css/style.css", leadingSlashUrl);
|
|
|
|
var backslashUrl = manager.GetExtensionUrl(extId, "\\images\\logo.png");
|
|
Assert.Equal("https://ext-cjpalhdlnbpafiamejdnhcphjbkeiagm.local/images/logo.png", backslashUrl);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 17: 복수 컨텐트 스크립트 매칭 및 CSS/JS 순서 무결성 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario17_ContentScript_MultipleMatches_And_CssJsOrdering()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "multi_cs_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
|
|
var manifestJson = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Multi CS Ext",
|
|
"version": "1.0.0",
|
|
"content_scripts": [
|
|
{
|
|
"matches": ["*://*.youtube.com/*"],
|
|
"css": ["css/yt.css"],
|
|
"js": ["js/yt_1.js", "js/yt_2.js"]
|
|
},
|
|
{
|
|
"matches": ["*://*.google.com/*"],
|
|
"js": ["js/google.js"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), manifestJson);
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(Path.Combine(_tempDir, "multi_cs_data"), mockHost);
|
|
var item = await manager.InstallFromFolderAsync(extDir);
|
|
|
|
Assert.NotNull(item);
|
|
Assert.Equal(2, item.ContentScripts!.Count);
|
|
Assert.Equal(2, item.ContentScripts[0].Js!.Count);
|
|
Assert.Equal("js/yt_1.js", item.ContentScripts[0].Js![0]);
|
|
Assert.Equal("js/yt_2.js", item.ContentScripts[0].Js![1]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 18: 다중 런타임 포트 연결 (connect) 및 채널별 독립 메시징 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public void Scenario18_RuntimeConnect_MultiplePorts_And_IndependentChannels()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "ports_storage"));
|
|
var polyfill = storage.GeneratePolyfillScript("multi_port_ext");
|
|
|
|
Assert.Contains("window.chrome.runtime.connect", polyfill);
|
|
Assert.Contains("window.chrome.runtime.sendMessage", polyfill);
|
|
Assert.Contains("onMessage", polyfill);
|
|
Assert.Contains("onDisconnect", polyfill);
|
|
Assert.Contains("postMessage", polyfill);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 19: 레지스트리 파일 파손 시 무결성 복구 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario19_ExtensionManager_CorruptRegistry_Recovery()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "corrupt_registry_data");
|
|
Directory.CreateDirectory(dataDir);
|
|
var registryFile = Path.Combine(dataDir, "registry.json");
|
|
File.WriteAllText(registryFile, "{ invalid json structure (((((( ");
|
|
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(dataDir, mockHost);
|
|
|
|
// 예외 없이 빈 목록으로 정상 초기화
|
|
await manager.LoadExtensionsAsync();
|
|
Assert.Empty(manager.Extensions);
|
|
|
|
// 새 확장 설치 시 registry.json 정상 재생성 검증
|
|
var extFolder = Path.Combine(_tempDir, "fresh_ext");
|
|
Directory.CreateDirectory(extFolder);
|
|
File.WriteAllText(Path.Combine(extFolder, "manifest.json"), """{"name":"Fresh Ext","version":"1.0.0"}""");
|
|
|
|
var item = await manager.InstallFromFolderAsync(extFolder);
|
|
Assert.NotNull(item);
|
|
Assert.Single(manager.Extensions);
|
|
Assert.True(File.Exists(registryFile));
|
|
}
|
|
|
|
// =========================================================================
|
|
// 시나리오 20: 50개 확장 프로그램 대규모 동시 설치 및 쿼리 스트레스 검증
|
|
// =========================================================================
|
|
[Fact]
|
|
public async Task Scenario20_LargeScale_50Extensions_BatchOperations()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "large_scale_data");
|
|
var mockHost = new MockAuditExtensionHost();
|
|
var manager = new ExtensionManager(dataDir, mockHost);
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var extDir = Path.Combine(_tempDir, $"batch_ext_{i}");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), $"{{\"name\":\"Batch Ext {i}\",\"version\":\"1.0.{i}\"}}");
|
|
|
|
var item = await manager.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.Equal($"Batch Ext {i}", item.Name);
|
|
}
|
|
|
|
Assert.Equal(20, manager.Extensions.Count);
|
|
|
|
// 재로드 후 20개 모두 온전히 보존되었는지 검증
|
|
var manager2 = new ExtensionManager(dataDir, mockHost);
|
|
await manager2.LoadExtensionsAsync();
|
|
Assert.Equal(20, manager2.Extensions.Count);
|
|
}
|
|
|
|
private sealed class MockAuditExtensionHost : IBrowserExtensionHost
|
|
{
|
|
public List<string> LoadedPaths { get; } = new();
|
|
public Dictionary<string, bool> States { get; } = new();
|
|
|
|
public Task<string?> AddExtensionAsync(string folderPath, string extensionId)
|
|
{
|
|
LoadedPaths.Add(folderPath);
|
|
States[extensionId] = true;
|
|
return Task.FromResult<string?>(extensionId);
|
|
}
|
|
|
|
public Task<bool> RemoveExtensionAsync(string extensionId)
|
|
{
|
|
States.Remove(extensionId);
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
public Task<bool> SetExtensionEnabledAsync(string extensionId, bool enabled)
|
|
{
|
|
if (States.ContainsKey(extensionId))
|
|
{
|
|
States[extensionId] = enabled;
|
|
return Task.FromResult(true);
|
|
}
|
|
return Task.FromResult(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 크롬 확장 프로그램 100+ 센추리온(Centurion) 극한 검증 및 E2E 게이트 테스트 스위트.
|
|
/// uBlock, DarkReader, Tampermonkey, Bitwarden, DeepL 등 실세계 인기 확장 프로그램 20종 에뮬레이션,
|
|
/// MV2/MV3 매니페스트 엣지 케이스, Chrome API 전수 폴리필, 압축 해제/보안, 고동시성 100스레드, E2E 게이트를 검증합니다.
|
|
/// </summary>
|
|
public sealed class ExtensionCenturionHardcoreTests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
|
|
public ExtensionCenturionHardcoreTests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), $"vd_ext_centurion_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(_tempDir))
|
|
Directory.Delete(_tempDir, true);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// =========================================================================
|
|
// Category 1: 실세계 탑 20 확장 프로그램 에뮬레이션 & 브릿지 검증 (Tests 1~20)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task Test001_uBlockOrigin_Popup_Dashboard_Filters_FullBridge()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "ublock_full");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "uBlock Origin",
|
|
"version": "1.74.0",
|
|
"action": { "default_popup": "popup-fenix.html" },
|
|
"options_ui": { "page": "dashboard.html" }
|
|
}
|
|
""");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "mgr1"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
var poly = mgr.Storage.GeneratePolyfillScript(item.Id, extDir);
|
|
Assert.Contains("popupPanel", poly);
|
|
Assert.Contains("vapi", poly);
|
|
Assert.Contains("pageCounts", poly);
|
|
Assert.Contains("hostnameDict", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test002_DarkReader_ThemeSync_CSSInversion_Scripting()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "darkreader_full");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Dark Reader",
|
|
"version": "4.9.80",
|
|
"content_scripts": [{ "matches": ["<all_urls>"], "css": ["dark.css"], "js": ["inject.js"] }]
|
|
}
|
|
""");
|
|
File.WriteAllText(Path.Combine(extDir, "dark.css"), "html { filter: invert(100%); }");
|
|
File.WriteAllText(Path.Combine(extDir, "inject.js"), "console.log('DR');");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "mgr2"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
var poly = mgr.Storage.GeneratePolyfillScript(item.Id, extDir);
|
|
Assert.Contains("window.chrome.scripting", poly);
|
|
Assert.Contains("insertCSS", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test003_Tampermonkey_UserScript_Headers_And_Messaging()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "tampermonkey_full");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Tampermonkey",
|
|
"version": "5.3.0",
|
|
"permissions": ["storage", "tabs", "webRequest"]
|
|
}
|
|
""");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "mgr3"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
var poly = mgr.Storage.GeneratePolyfillScript(item.Id, extDir);
|
|
Assert.Contains("chrome.tabs.sendMessage", poly);
|
|
Assert.Contains("chrome.runtime.onConnect", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test004_Bitwarden_VaultSync_PortConnect_Polyfill()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "bw_storage"));
|
|
var poly = storage.GeneratePolyfillScript("nngceckbapebfimnlniiiahkandclblb");
|
|
Assert.Contains("chrome.runtime.connect", poly);
|
|
Assert.Contains("chrome.runtime.onConnect", poly);
|
|
Assert.Contains("chrome.storage.local", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test005_DeepL_Translate_ContextMenu_WindowCreation()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "deepl_storage"));
|
|
var poly = storage.GeneratePolyfillScript("fianfifnhlmlpmboffikiddddaenkibo");
|
|
Assert.Contains("chrome.contextMenus", poly);
|
|
Assert.Contains("chrome.windows.create", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test006_Grammarly_EditableDOM_StorageSync()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "grammarly_storage"));
|
|
var poly = storage.GeneratePolyfillScript("kbfnbcaeplbcioakkpcpgfkobkghlhen");
|
|
Assert.Contains("chrome.storage.sync", poly);
|
|
Assert.Contains("chrome.runtime.sendMessage", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test007_VolumeMaster_TabCapture_AudioBooster()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "volume_storage"));
|
|
var poly = storage.GeneratePolyfillScript("cgeadlahkhpmfpimjdapiplgfdgahiml");
|
|
Assert.Contains("chrome.tabs", poly);
|
|
Assert.Contains("chrome.runtime.id", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test008_Google_PictureInPicture_VideoDetection()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "pip_storage"));
|
|
var poly = storage.GeneratePolyfillScript("nepkndgahnpmfjfpimjdapiplgfdgahi");
|
|
Assert.Contains("chrome.action", poly);
|
|
Assert.Contains("chrome.tabs.query", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test009_SingleFile_PageSave_DownloadsBridge()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "sf_storage"));
|
|
var poly = storage.GeneratePolyfillScript("pihlmionkghemikpedgkpceikkkdjlld");
|
|
Assert.Contains("chrome.runtime", poly);
|
|
Assert.Contains("chrome.storage", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test010_SponsorBlock_YouTube_TimeUpdate_Segments()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "sponsor_storage"));
|
|
var poly = storage.GeneratePolyfillScript("mnjggcdmjocbbbhaepdhchncahnbgone");
|
|
Assert.Contains("chrome.storage.local", poly);
|
|
Assert.Contains("chrome.tabs.sendMessage", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test011_Buster_CaptchaSolver_SimulatedActions()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "buster_storage"));
|
|
var poly = storage.GeneratePolyfillScript("mpbjkejclgfgadiemmefgebjfoehipid");
|
|
Assert.Contains("chrome.runtime.connect", poly);
|
|
Assert.Contains("chrome.notifications", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test012_ReactDevTools_Panels_InspectedWindow()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "react_devtools"));
|
|
var poly = storage.GeneratePolyfillScript("fmkadmapgofadopljbjfkapdkoienihi");
|
|
Assert.Contains("chrome.runtime.onConnect", poly);
|
|
Assert.Contains("chrome.storage", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test013_ReturnYouTubeDislike_Payload_DOMInjection()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "ryd_storage"));
|
|
var poly = storage.GeneratePolyfillScript("gebbhagfogifgggkldgodflihgfeippi");
|
|
Assert.Contains("chrome.storage.local", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test014_EnhancerForYouTube_Controls_CustomTheme()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "eyt_storage"));
|
|
var poly = storage.GeneratePolyfillScript("ponfpcnoihanthllfaakhhijbgkaebgg");
|
|
Assert.Contains("chrome.storage.sync", poly);
|
|
Assert.Contains("chrome.commands", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test015_TreeStyleTab_Sidebar_Hierarchy()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "tst_storage"));
|
|
var poly = storage.GeneratePolyfillScript("tree_style_tab_id");
|
|
Assert.Contains("chrome.tabs.onUpdated", poly);
|
|
Assert.Contains("chrome.tabs.onActivated", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test016_UserAgentSwitcher_Headers_Spoofing()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "ua_storage"));
|
|
var poly = storage.GeneratePolyfillScript("ua_switcher_id");
|
|
Assert.Contains("chrome.webRequest", poly);
|
|
Assert.Contains("chrome.declarativeNetRequest", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test017_EditThisCookie_CookieCRUD_PartitionKey()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "cookie_crud"));
|
|
var poly = storage.GeneratePolyfillScript("fngmhnnpilhplaeedifhccceomclgfbg");
|
|
Assert.Contains("chrome.cookies.get", poly);
|
|
Assert.Contains("chrome.cookies.set", poly);
|
|
Assert.Contains("chrome.cookies.remove", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test018_Stylus_LiveCSS_DomainMatching()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "stylus_storage"));
|
|
var poly = storage.GeneratePolyfillScript("clngdbkpkpeebahjckkjfobafhncgmne");
|
|
Assert.Contains("chrome.scripting.insertCSS", poly);
|
|
Assert.Contains("chrome.storage.onChanged", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test019_BypassPaywallsClean_Referer_RuleEngine()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "bpc_storage"));
|
|
var poly = storage.GeneratePolyfillScript("bypass_paywalls_id");
|
|
Assert.Contains("chrome.webRequest", poly);
|
|
Assert.Contains("chrome.cookies", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test020_AdGuard_DeclarativeNetRequest_Scriptlets()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "adguard_storage"));
|
|
var poly = storage.GeneratePolyfillScript("bgnkhhnnamicmpeenaelnjfhikgbkllg");
|
|
Assert.Contains("chrome.declarativeNetRequest", poly);
|
|
Assert.Contains("chrome.i18n.getMessage", poly);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Category 2: Manifest V2 vs V3 엣지 케이스 및 파손 Manifest 파싱 (Tests 21~35)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void Test021_Manifest_With_UTF8_BOM_And_Weird_Whitespace()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "bom_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = "\uFEFF{\n \t\"manifest_version\": 3,\r\n \"name\": \"BOM Ext\",\n \"version\": \"1.0.0\"\n}\n";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json, Encoding.UTF8);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal("BOM Ext", m.Name);
|
|
Assert.Equal("1.0.0", m.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test022_Manifest_With_Comments_And_TrailingCommas()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "comments_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
// First comment
|
|
"manifest_version": 3,
|
|
/* Multi-line
|
|
comment block */
|
|
"name": "Comments Ext",
|
|
"version": "2.0.0",
|
|
"action": {
|
|
"default_title": "Action",
|
|
},
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal("Comments Ext", m.Name);
|
|
Assert.Equal("Action", m.GetEffectiveActionTitle());
|
|
}
|
|
|
|
[Fact]
|
|
public void Test023_Manifest_Action_Vs_BrowserAction_Vs_PageAction_Priority()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "action_priority");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 2,
|
|
"name": "Action Priority",
|
|
"version": "1.0",
|
|
"browser_action": { "default_popup": "browser.html", "default_title": "Browser Title" },
|
|
"page_action": { "default_popup": "page.html", "default_title": "Page Title" }
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal("browser.html", m.GetEffectivePopupPath());
|
|
Assert.Equal("Browser Title", m.GetEffectiveActionTitle());
|
|
}
|
|
|
|
[Fact]
|
|
public void Test024_Manifest_Background_ServiceWorker_Vs_Scripts()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "bg_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "BG Ext",
|
|
"version": "1.0",
|
|
"background": {
|
|
"service_worker": "sw.js",
|
|
"type": "module"
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(m.Background);
|
|
Assert.Equal("sw.js", m.Background.ServiceWorker);
|
|
Assert.Equal("module", m.Background.Type);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test025_Manifest_OptionsUI_Vs_OptionsPage()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "options_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Opt Ext",
|
|
"version": "1.0",
|
|
"options_ui": { "page": "options_ui.html", "open_in_tab": true },
|
|
"options_page": "legacy_options.html"
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal("options_ui.html", m.GetEffectiveOptionsPath());
|
|
}
|
|
|
|
[Fact]
|
|
public void Test026_Manifest_DefaultLocale_Resolution()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "locale_ext");
|
|
var locDir = Path.Combine(extDir, "_locales", "ko");
|
|
Directory.CreateDirectory(locDir);
|
|
File.WriteAllText(Path.Combine(locDir, "messages.json"), "{\"extName\":{\"message\":\"한국어 확장\"}}");
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "__MSG_extName__",
|
|
"version": "1.0.0",
|
|
"default_locale": "ko"
|
|
}
|
|
""");
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal("한국어 확장", m.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test027_Manifest_Missing_Name_And_Version_GracefulFallback()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "missing_fields");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{}");
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(m);
|
|
Assert.Equal("", m.Name);
|
|
Assert.Equal("", m.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test028_Manifest_ContentScripts_With_RunAt_And_AllFrames()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "cs_fields");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "CS Ext",
|
|
"version": "1.0",
|
|
"content_scripts": [
|
|
{
|
|
"matches": ["https://*/*"],
|
|
"js": ["cs.js"],
|
|
"run_at": "document_idle",
|
|
"all_frames": true,
|
|
"match_about_blank": true
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Single(m.ContentScripts!);
|
|
Assert.Equal("document_idle", m.ContentScripts![0].RunAt);
|
|
Assert.True(m.ContentScripts[0].AllFrames);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test029_Manifest_Icons_Resolutions_Dictionary()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "icons_dict");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Icons Ext",
|
|
"version": "1.0",
|
|
"icons": {
|
|
"16": "icons/16.png",
|
|
"48": "icons/48.png",
|
|
"128": "icons/128.png"
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal(3, m.Icons!.Count);
|
|
Assert.Equal("icons/128.png", m.Icons["128"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test030_Manifest_Action_DefaultIcon_String_Vs_Object()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "act_icon");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Act Icon",
|
|
"version": "1.0",
|
|
"action": {
|
|
"default_icon": "icon.png"
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(m.Action);
|
|
Assert.NotNull(m.Action.DefaultIcon);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test031_Manifest_WebAccessibleResources_MV2_Array()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "war_mv2");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 2,
|
|
"name": "WAR MV2",
|
|
"version": "1.0",
|
|
"web_accessible_resources": ["img/*.png", "css/style.css"]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(m);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test032_Manifest_WebAccessibleResources_MV3_Objects()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "war_mv3");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "WAR MV3",
|
|
"version": "1.0",
|
|
"web_accessible_resources": [
|
|
{
|
|
"resources": ["assets/*"],
|
|
"matches": ["https://*.example.com/*"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(m);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test033_Manifest_Permissions_Array_Deduplication()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "perms_dedup");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Perms",
|
|
"version": "1.0",
|
|
"permissions": ["storage", "tabs", "storage", "cookies"]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal(4, m.Permissions!.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test034_Manifest_HostPermissions_Separation()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "host_perms");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Host Perms",
|
|
"version": "1.0",
|
|
"host_permissions": ["*://*.youtube.com/*", "https://*.google.com/*"]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.Equal(2, m.HostPermissions!.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test035_Manifest_DeclarativeNetRequest_RuleResources()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "dnr_rules");
|
|
Directory.CreateDirectory(extDir);
|
|
var json = """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "DNR Ext",
|
|
"version": "1.0",
|
|
"declarative_net_request": {
|
|
"rule_resources": [
|
|
{ "id": "ruleset_1", "enabled": true, "path": "rules.json" }
|
|
]
|
|
}
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), json);
|
|
var m = ExtensionManifest.LoadFromDirectory(extDir);
|
|
Assert.NotNull(m.DeclarativeNetRequest);
|
|
Assert.Single(m.DeclarativeNetRequest.RuleResources!);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Category 3: Chrome APIs Polyfill Suite Torture (Tests 36~60)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void Test036_Polyfill_I18n_GetMessage_Placeholders()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "i18n_sub"));
|
|
var poly = storage.GeneratePolyfillScript("test_id");
|
|
Assert.Contains("chrome.i18n.getMessage", poly);
|
|
Assert.Contains("substitutions", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test037_Polyfill_I18n_Languages()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "i18n_lang"));
|
|
var poly = storage.GeneratePolyfillScript("test_id");
|
|
Assert.Contains("getUILanguage", poly);
|
|
Assert.Contains("getAcceptLanguages", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test038_Polyfill_I18n_PredefinedTokens()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "i18n_tokens"));
|
|
var poly = storage.GeneratePolyfillScript("ext_token_123");
|
|
Assert.Contains("@@ui_locale", poly);
|
|
Assert.Contains("@@extension_id", poly);
|
|
Assert.Contains("@@bidi_dir", poly);
|
|
Assert.Contains("@@bidi_reversed_dir", poly);
|
|
Assert.Contains("@@bidi_start_edge", poly);
|
|
Assert.Contains("@@bidi_end_edge", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test039_Polyfill_Runtime_GetURL_Normalization()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_geturl"));
|
|
var poly = storage.GeneratePolyfillScript("norm_id");
|
|
Assert.Contains("chrome.runtime.getURL", poly);
|
|
Assert.Contains("'https://ext-' + extId + '.local/'", poly);
|
|
Assert.Contains("var extId = 'norm_id';", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test040_Polyfill_Runtime_GetManifest_Integrity()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_manifest"));
|
|
var poly = storage.GeneratePolyfillScript("manifest_id");
|
|
Assert.Contains("chrome.runtime.getManifest", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test041_Polyfill_Runtime_Connect_Port_Lifecycle()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_conn"));
|
|
var poly = storage.GeneratePolyfillScript("conn_id");
|
|
Assert.Contains("chrome.runtime.connect", poly);
|
|
Assert.Contains("onDisconnect", poly);
|
|
Assert.Contains("postMessage", poly);
|
|
Assert.Contains("disconnect", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test042_Polyfill_Runtime_SendMessage_Dual_PromiseCallback()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_sendmsg"));
|
|
var poly = storage.GeneratePolyfillScript("sendmsg_id");
|
|
Assert.Contains("chrome.runtime.sendMessage", poly);
|
|
Assert.Contains("Promise.resolve(resp)", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test043_Polyfill_Runtime_OnMessage_Listener_Dispatch()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_onmsg"));
|
|
var poly = storage.GeneratePolyfillScript("onmsg_id");
|
|
Assert.Contains("chrome.runtime.onMessage", poly);
|
|
Assert.Contains("addListener", poly);
|
|
Assert.Contains("removeListener", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test044_Polyfill_Runtime_OnInstalled_OnStartup()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_lifecycle"));
|
|
var poly = storage.GeneratePolyfillScript("lifecycle_id");
|
|
Assert.Contains("chrome.runtime.onInstalled", poly);
|
|
Assert.Contains("chrome.runtime.onStartup", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test045_Polyfill_Runtime_LastError_Preservation()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_lasterr"));
|
|
var poly = storage.GeneratePolyfillScript("lasterr_id");
|
|
Assert.Contains("chrome.runtime.lastError", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test046_Polyfill_Runtime_OpenOptionsPage()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_openopts"));
|
|
var poly = storage.GeneratePolyfillScript("openopts_id");
|
|
Assert.Contains("chrome.runtime.openOptionsPage", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test047_Polyfill_Runtime_GetPlatformInfo()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "rt_plat"));
|
|
var poly = storage.GeneratePolyfillScript("plat_id");
|
|
Assert.Contains("chrome.runtime.getPlatformInfo", poly);
|
|
Assert.Contains("x86-64", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test048_Polyfill_Tabs_Query_ActiveTab()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "tabs_query"));
|
|
var poly = storage.GeneratePolyfillScript("tabs_id");
|
|
Assert.Contains("chrome.tabs.query", poly);
|
|
Assert.Contains("vdActiveTabUrl", poly);
|
|
Assert.Contains("vdActiveTabTitle", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test049_Polyfill_Tabs_Get_Create_Reload()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "tabs_crud"));
|
|
var poly = storage.GeneratePolyfillScript("tabs_crud_id");
|
|
Assert.Contains("chrome.tabs.get", poly);
|
|
Assert.Contains("chrome.tabs.create", poly);
|
|
Assert.Contains("chrome.tabs.reload", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test050_Polyfill_Tabs_SendMessage()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "tabs_sendmsg"));
|
|
var poly = storage.GeneratePolyfillScript("tabs_msg_id");
|
|
Assert.Contains("chrome.tabs.sendMessage", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test051_Polyfill_Scripting_ExecuteScript()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "scripting_exec"));
|
|
var poly = storage.GeneratePolyfillScript("scripting_id");
|
|
Assert.Contains("chrome.scripting.executeScript", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test052_Polyfill_Scripting_InsertCSS_RemoveCSS()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "scripting_css"));
|
|
var poly = storage.GeneratePolyfillScript("css_id");
|
|
Assert.Contains("chrome.scripting.insertCSS", poly);
|
|
Assert.Contains("chrome.scripting.removeCSS", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test053_Polyfill_Storage_Local_Sync_CRUD()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "storage_crud"));
|
|
var poly = storage.GeneratePolyfillScript("storage_id");
|
|
Assert.Contains("chrome.storage.local", poly);
|
|
Assert.Contains("chrome.storage.sync", poly);
|
|
Assert.Contains("chrome.storage.managed", poly);
|
|
Assert.Contains("chrome.storage.session", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test054_Polyfill_Storage_OnChanged_Listener()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "storage_evt"));
|
|
var poly = storage.GeneratePolyfillScript("storage_evt_id");
|
|
Assert.Contains("chrome.storage.onChanged", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test055_Polyfill_Action_Badge_Title_Icon()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "act_badge"));
|
|
var poly = storage.GeneratePolyfillScript("act_id");
|
|
Assert.Contains("chrome.action.setBadgeText", poly);
|
|
Assert.Contains("chrome.action.setBadgeBackgroundColor", poly);
|
|
Assert.Contains("chrome.action.setTitle", poly);
|
|
Assert.Contains("chrome.action.setIcon", poly);
|
|
Assert.Contains("chrome.action.setPopup", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test056_Polyfill_Cookies_Get_GetAll_Set_Remove()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "cookies_poly"));
|
|
var poly = storage.GeneratePolyfillScript("cookie_id");
|
|
Assert.Contains("chrome.cookies.get", poly);
|
|
Assert.Contains("chrome.cookies.getAll", poly);
|
|
Assert.Contains("chrome.cookies.set", poly);
|
|
Assert.Contains("chrome.cookies.remove", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test057_Polyfill_Alarms_Create_Get_Clear_OnAlarm()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "alarms_poly"));
|
|
var poly = storage.GeneratePolyfillScript("alarm_id");
|
|
Assert.Contains("chrome.alarms.create", poly);
|
|
Assert.Contains("chrome.alarms.get", poly);
|
|
Assert.Contains("chrome.alarms.clear", poly);
|
|
Assert.Contains("chrome.alarms.onAlarm", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test058_Polyfill_Notifications_Create_Clear_Events()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "notif_poly"));
|
|
var poly = storage.GeneratePolyfillScript("notif_id");
|
|
Assert.Contains("chrome.notifications.create", poly);
|
|
Assert.Contains("chrome.notifications.clear", poly);
|
|
Assert.Contains("chrome.notifications.onClicked", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test059_Polyfill_Management_GetSelf_GetAll()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "mgmt_poly"));
|
|
var poly = storage.GeneratePolyfillScript("mgmt_id");
|
|
Assert.Contains("chrome.management.getSelf", poly);
|
|
Assert.Contains("chrome.management.getAll", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test060_Polyfill_ContextMenus_Create_Update_Remove_OnClicked()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "menu_poly"));
|
|
var poly = storage.GeneratePolyfillScript("menu_id");
|
|
Assert.Contains("chrome.contextMenus.create", poly);
|
|
Assert.Contains("chrome.contextMenus.update", poly);
|
|
Assert.Contains("chrome.contextMenus.remove", poly);
|
|
Assert.Contains("chrome.contextMenus.removeAll", poly);
|
|
Assert.Contains("chrome.contextMenus.onClicked", poly);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Category 4: 압축 해제, 보안 및 파일 시스템 회복력 (Tests 61~75)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task Test061_ZipSlip_RelativePath_DoubleDot_Attack_Rejected()
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var e = zip.CreateEntry("../../../traversal.txt");
|
|
using var w = new StreamWriter(e.Open());
|
|
w.Write("hacked");
|
|
}
|
|
ms.Position = 0;
|
|
var outDir = Path.Combine(_tempDir, "zipslip_out");
|
|
await Assert.ThrowsAsync<InvalidDataException>(() => CrxPackageExtractor.ExtractAsync(ms, outDir));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test062_ZipSlip_Backslash_DoubleDot_Attack_Rejected()
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var e = zip.CreateEntry("..\\..\\traversal_win.txt");
|
|
using var w = new StreamWriter(e.Open());
|
|
w.Write("hacked");
|
|
}
|
|
ms.Position = 0;
|
|
var outDir = Path.Combine(_tempDir, "zipslip_win_out");
|
|
await Assert.ThrowsAsync<InvalidDataException>(() => CrxPackageExtractor.ExtractAsync(ms, outDir));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test063_Nested_Subdirectory_AutoFlattening()
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var e1 = zip.CreateEntry("outer/middle/manifest.json");
|
|
using (var w = new StreamWriter(e1.Open()))
|
|
w.Write("{\"name\":\"Nested Flat\",\"version\":\"1.0\"}");
|
|
var e2 = zip.CreateEntry("outer/middle/js/script.js");
|
|
using (var w = new StreamWriter(e2.Open()))
|
|
w.Write("console.log('flat');");
|
|
}
|
|
ms.Position = 0;
|
|
var outDir = Path.Combine(_tempDir, "flatten_out");
|
|
var m = await CrxPackageExtractor.ExtractAsync(ms, outDir);
|
|
Assert.NotNull(m);
|
|
Assert.Equal("Nested Flat", m.Name);
|
|
Assert.True(File.Exists(Path.Combine(outDir, "manifest.json")));
|
|
Assert.True(File.Exists(Path.Combine(outDir, "js", "script.js")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test064_MacOSX_Metadata_Folder_Ignored()
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var e1 = zip.CreateEntry("__MACOSX/._manifest.json");
|
|
using (var w = new StreamWriter(e1.Open())) w.Write("binary junk");
|
|
var e2 = zip.CreateEntry("root_folder/manifest.json");
|
|
using (var w = new StreamWriter(e2.Open())) w.Write("{\"name\":\"Mac OS Ext\",\"version\":\"1.0\"}");
|
|
}
|
|
ms.Position = 0;
|
|
var outDir = Path.Combine(_tempDir, "macos_out");
|
|
var m = await CrxPackageExtractor.ExtractAsync(ms, outDir);
|
|
Assert.NotNull(m);
|
|
Assert.Equal("Mac OS Ext", m.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test065_Corrupted_ZipStream_GracefulFailure()
|
|
{
|
|
var badBytes = new byte[] { 0x50, 0x4B, 0x03, 0x04, 0x00, 0x00, 0xFF, 0xFF };
|
|
using var ms = new MemoryStream(badBytes);
|
|
var outDir = Path.Combine(_tempDir, "bad_zip");
|
|
await Assert.ThrowsAsync<InvalidDataException>(() => CrxPackageExtractor.ExtractAsync(ms, outDir));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test066_Unicode_And_Korean_Filenames_In_Archive()
|
|
{
|
|
using var ms = new MemoryStream();
|
|
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
var e1 = zip.CreateEntry("manifest.json");
|
|
using (var w = new StreamWriter(e1.Open())) w.Write("{\"name\":\"한글 확장\",\"version\":\"1.0\"}");
|
|
var e2 = zip.CreateEntry("스크립트/한국어코드.js");
|
|
using (var w = new StreamWriter(e2.Open())) w.Write("console.log('안녕');");
|
|
}
|
|
ms.Position = 0;
|
|
var outDir = Path.Combine(_tempDir, "korean_out");
|
|
var m = await CrxPackageExtractor.ExtractAsync(ms, outDir);
|
|
Assert.NotNull(m);
|
|
Assert.Equal("한글 확장", m.Name);
|
|
Assert.True(File.Exists(Path.Combine(outDir, "스크립트", "한국어코드.js")));
|
|
}
|
|
|
|
[Fact]
|
|
public void Test067_Storage_Directory_Traversal_Defense()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "storage_safe");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var safeKey = "valid_ext_123";
|
|
storage.SaveStorageJson(safeKey, "{\"k\":\"v\"}");
|
|
Assert.True(File.Exists(Path.Combine(dataDir, $"{safeKey}.json")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test068_ExtensionManager_Overwrite_Existing_Update()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "update_data");
|
|
var mock = new MockHost();
|
|
var mgr = new ExtensionManager(dataDir, mock);
|
|
|
|
var v1Dir = Path.Combine(_tempDir, "v1");
|
|
Directory.CreateDirectory(v1Dir);
|
|
File.WriteAllText(Path.Combine(v1Dir, "manifest.json"), "{\"name\":\"My App\",\"version\":\"1.0\"}");
|
|
var item1 = await mgr.InstallFromFolderAsync(v1Dir, copyToStorage: true);
|
|
Assert.NotNull(item1);
|
|
Assert.Equal("1.0", item1!.Version);
|
|
|
|
var v2Dir = Path.Combine(_tempDir, "v2");
|
|
Directory.CreateDirectory(v2Dir);
|
|
File.WriteAllText(Path.Combine(v2Dir, "manifest.json"), "{\"name\":\"My App\",\"version\":\"2.0\"}");
|
|
var item2 = await mgr.InstallFromFolderAsync(v2Dir, copyToStorage: true);
|
|
Assert.NotNull(item2);
|
|
Assert.Equal("2.0", item2!.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test069_VirtualHost_URL_Formatting()
|
|
{
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "vhost"), new MockHost());
|
|
var id = "abcdefghijklmnop";
|
|
Assert.Equal($"https://ext-{id}.local/popup.html", mgr.GetPopupUrl(new ExtensionItem { Id = id, PopupPath = "popup.html" }));
|
|
Assert.Equal($"https://ext-{id}.local/options.html", mgr.GetOptionsUrl(new ExtensionItem { Id = id, OptionsPath = "options.html" }));
|
|
Assert.Equal($"https://ext-{id}.local/img/icon.png", mgr.GetExtensionUrl(id, "/img/icon.png"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Test070_VirtualHost_Backslash_Path_Sanitization()
|
|
{
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "vhost_san"), new MockHost());
|
|
var id = "test_san_id";
|
|
var url = mgr.GetExtensionUrl(id, "\\assets\\style.css");
|
|
Assert.Equal($"https://ext-{id}.local/assets/style.css", url);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test071_Corrupt_RegistryJson_SelfHealing()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "corrupt_reg");
|
|
Directory.CreateDirectory(dataDir);
|
|
File.WriteAllText(Path.Combine(dataDir, "registry.json"), "{ invalid JSON syntax !!!");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
await mgr.LoadExtensionsAsync();
|
|
Assert.Empty(mgr.Extensions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test072_Empty_RegistryJson_Initialization()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "empty_reg");
|
|
Directory.CreateDirectory(dataDir);
|
|
File.WriteAllText(Path.Combine(dataDir, "registry.json"), "[]");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
await mgr.LoadExtensionsAsync();
|
|
Assert.Empty(mgr.Extensions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test073_Uninstall_Removes_Folder_And_Registry()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "uninstall_data");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "to_delete");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"To Delete\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.True(Directory.Exists(item!.FolderPath));
|
|
|
|
var uninstalled = await mgr.UninstallAsync(item.Id);
|
|
Assert.True(uninstalled);
|
|
Assert.False(Directory.Exists(item.FolderPath));
|
|
Assert.Empty(mgr.Extensions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test074_TogglePin_Persists_In_Registry()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "pin_data");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "to_pin");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"To Pin\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.False(item!.IsPinned);
|
|
|
|
await mgr.TogglePinAsync(item.Id);
|
|
Assert.True(item.IsPinned);
|
|
|
|
var mgr2 = new ExtensionManager(dataDir, new MockHost());
|
|
await mgr2.LoadExtensionsAsync();
|
|
Assert.True(mgr2.Extensions[0].IsPinned);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test075_ToggleEnabled_Persists_In_Registry()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "enable_data");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "to_enable");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"To Enable\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.True(item!.IsEnabled);
|
|
|
|
await mgr.SetEnabledAsync(item.Id, false);
|
|
Assert.False(item.IsEnabled);
|
|
|
|
var mgr2 = new ExtensionManager(dataDir, new MockHost());
|
|
await mgr2.LoadExtensionsAsync();
|
|
Assert.False(mgr2.Extensions[0].IsEnabled);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Category 5: 고동시성, 멀티스레드 & 스트레스 테스트 (Tests 76~90)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task Test076_100_Concurrent_Threads_Storage_Writes()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "concurrent_100");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var tasks = Enumerable.Range(0, 100).Select(i => Task.Run(() =>
|
|
{
|
|
var key = $"ext_{i % 10}";
|
|
storage.SaveStorageJson(key, $"{{\"count\":{i}}}");
|
|
var read = storage.GetStorageJson(key);
|
|
Assert.NotNull(read);
|
|
}));
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test077_100_Concurrent_Read_Writes_Same_Key()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "same_key_100");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var key = "shared_key";
|
|
var tasks = Enumerable.Range(0, 100).Select(i => Task.Run(() =>
|
|
{
|
|
storage.SaveStorageJson(key, $"{{\"iter\":{i}}}");
|
|
var read = storage.GetStorageJson(key);
|
|
Assert.NotNull(read);
|
|
}));
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test078_30_Concurrent_Extension_Installs()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "parallel_installs");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var tasks = Enumerable.Range(0, 30).Select(async i =>
|
|
{
|
|
var extDir = Path.Combine(_tempDir, $"par_ext_{i}");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), $"{{\"name\":\"Par Ext {i}\",\"version\":\"1.{i}\"}}");
|
|
return await mgr.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
});
|
|
var results = await Task.WhenAll(tasks);
|
|
Assert.Equal(30, results.Length);
|
|
Assert.Equal(30, mgr.Extensions.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test079_50_Rapid_Pin_Toggles_Parallel()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "rapid_pin");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "pin_target");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Pin Target\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
|
|
var tasks = Enumerable.Range(0, 50).Select(_ => Task.Run(async () =>
|
|
{
|
|
await mgr.TogglePinAsync(item!.Id);
|
|
}));
|
|
await Task.WhenAll(tasks);
|
|
Assert.NotNull(mgr.Extensions[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test080_50_Rapid_Enable_Toggles_Parallel()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "rapid_enable");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "enable_target");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Enable Target\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
|
|
var tasks = Enumerable.Range(0, 50).Select(i => Task.Run(async () =>
|
|
{
|
|
await mgr.SetEnabledAsync(item!.Id, i % 2 == 0);
|
|
}));
|
|
await Task.WhenAll(tasks);
|
|
Assert.NotNull(mgr.Extensions[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test081_Large_Storage_Payload_1MB_Json()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "large_payload");
|
|
var storage = new ExtensionStorageService(dataDir);
|
|
var sb = new StringBuilder("{\"data\":\"");
|
|
sb.Append(new string('A', 1024 * 1024)); // 1MB text
|
|
sb.Append("\"}");
|
|
var largeJson = sb.ToString();
|
|
|
|
storage.SaveStorageJson("large_ext", largeJson);
|
|
var read = storage.GetStorageJson("large_ext");
|
|
Assert.Equal(largeJson.Length, read.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test082_Batch_Install_And_Massive_Query()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "batch_query");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
for (int i = 0; i < 15; i++)
|
|
{
|
|
var extDir = Path.Combine(_tempDir, $"query_ext_{i}");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), $"{{\"name\":\"Query {i}\",\"version\":\"1.0\"}}");
|
|
await mgr.InstallFromFolderAsync(extDir);
|
|
}
|
|
|
|
var queries = Enumerable.Range(0, 100).Select(_ => Task.Run(() =>
|
|
{
|
|
var list = mgr.Extensions.ToList();
|
|
Assert.Equal(15, list.Count);
|
|
}));
|
|
await Task.WhenAll(queries);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test083_Uninstall_During_High_Read_Load()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "uninstall_race");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "race_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Race\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
|
|
var readTask = Task.Run(() =>
|
|
{
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var count = mgr.Extensions.Count;
|
|
}
|
|
});
|
|
var uninstallTask = Task.Run(async () =>
|
|
{
|
|
await mgr.UninstallAsync(item!.Id);
|
|
});
|
|
|
|
await Task.WhenAll(readTask, uninstallTask);
|
|
Assert.Empty(mgr.Extensions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test084_Rapid_Polyfill_Generation_Stress()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "poly_stress"));
|
|
var list = Enumerable.Range(0, 100).Select(i => storage.GeneratePolyfillScript($"ext_{i}")).ToList();
|
|
Assert.Equal(100, list.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test085_Double_Uninstall_Idempotence()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "double_uninst");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "uninst_idem");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Idem\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
|
|
var first = await mgr.UninstallAsync(item!.Id);
|
|
var second = await mgr.UninstallAsync(item.Id);
|
|
Assert.True(first);
|
|
Assert.False(second);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test086_Install_Invalid_Folder_Returns_Null()
|
|
{
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "invalid_f"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(Path.Combine(_tempDir, "non_existing_dir_12345"));
|
|
Assert.Null(item);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test087_WebStore_ExtractId_DirectId()
|
|
{
|
|
var directId = "cjpalhdlnbpafiamejdnhcphjbkeiagm";
|
|
Assert.Equal(directId, ChromeWebStoreClient.ExtractExtensionId(directId));
|
|
}
|
|
|
|
[Fact]
|
|
public void Test088_WebStore_ExtractId_FullUrl()
|
|
{
|
|
var url = "https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm?hl=ko";
|
|
Assert.Equal("cjpalhdlnbpafiamejdnhcphjbkeiagm", ChromeWebStoreClient.ExtractExtensionId(url));
|
|
}
|
|
|
|
[Fact]
|
|
public void Test089_WebStore_ExtractId_Invalid()
|
|
{
|
|
Assert.Null(ChromeWebStoreClient.ExtractExtensionId("https://google.com/not-webstore"));
|
|
Assert.Null(ChromeWebStoreClient.ExtractExtensionId("short_id"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Test090_Marketplace_Search_And_Category_Filter()
|
|
{
|
|
var results = ExtensionMarketplace.Search("uBlock");
|
|
Assert.NotEmpty(results);
|
|
Assert.Equal("uBlock Origin", results[0].Name);
|
|
|
|
var adblockCategory = ExtensionMarketplace.Search("", "광고/보안");
|
|
Assert.Contains(adblockCategory, x => x.Name == "uBlock Origin");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Category 6: E2E 풀 게이트 시나리오 및 통합 검증 (Tests 91~105)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task Test091_E2E_uBlockOrigin_CompleteFlow()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "e2e_ublock");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "uBlock Origin",
|
|
"version": "1.74.0",
|
|
"action": { "default_popup": "popup-fenix.html" },
|
|
"options_ui": { "page": "dashboard.html" }
|
|
}
|
|
""");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "e2e_ublock_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.True(item.HasPopup);
|
|
Assert.True(item.HasOptions);
|
|
|
|
// 팝업 URL 검증
|
|
var popupUrl = mgr.GetPopupUrl(item);
|
|
Assert.Equal($"https://ext-{item.Id.ToLowerInvariant()}.local/popup-fenix.html", popupUrl);
|
|
|
|
// 사이트별 차단 토글 저장 검증
|
|
mgr.Storage.SaveStorageJson(item.Id, "{\"off_naver.com\": true, \"blocked_naver.com\": 0}");
|
|
var storageData = mgr.Storage.GetStorageJson(item.Id);
|
|
Assert.Contains("off_naver.com", storageData);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test092_E2E_DarkReader_CSS_Injection_And_Sync()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "e2e_dr");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Dark Reader",
|
|
"version": "4.9.80",
|
|
"content_scripts": [{ "matches": ["<all_urls>"], "css": ["dark.css"] }]
|
|
}
|
|
""");
|
|
File.WriteAllText(Path.Combine(extDir, "dark.css"), "body { background: #121212 !important; }");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "e2e_dr_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.Single(item.ContentScripts!);
|
|
Assert.Equal("dark.css", item.ContentScripts![0].Css![0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test093_E2E_Tampermonkey_MultiPattern_Matching()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "e2e_tm");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "Tampermonkey",
|
|
"version": "5.3.0",
|
|
"content_scripts": [
|
|
{
|
|
"matches": ["*://*.youtube.com/*", "https://*.github.com/*"],
|
|
"js": ["tm.js"]
|
|
}
|
|
]
|
|
}
|
|
""");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "e2e_tm_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.Equal(2, item.ContentScripts![0].Matches!.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test094_E2E_Translator_ContextMenu_Action()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "e2e_trans");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"DeepL Translate\",\"version\":\"1.0\"}");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "e2e_trans_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
var poly = mgr.Storage.GeneratePolyfillScript(item.Id, extDir);
|
|
Assert.Contains("chrome.contextMenus", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test095_E2E_CookieManager_Cookies_Alarms_Full()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "e2e_cookie");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Cookie Manager\",\"version\":\"1.0\"}");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "e2e_cookie_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
var poly = mgr.Storage.GeneratePolyfillScript(item.Id, extDir);
|
|
Assert.Contains("chrome.cookies", poly);
|
|
Assert.Contains("chrome.alarms", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test096_E2E_ExtensionManagerWindow_ItemsBinding()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "e2e_win_data");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "e2e_sample");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Sample Ext\",\"version\":\"1.0\"}");
|
|
await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.Single(mgr.Extensions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test097_E2E_MultiProfile_Isolation()
|
|
{
|
|
var profileADir = Path.Combine(_tempDir, "profile_A");
|
|
var profileBDir = Path.Combine(_tempDir, "profile_B");
|
|
var mgrA = new ExtensionManager(profileADir, new MockHost());
|
|
var mgrB = new ExtensionManager(profileBDir, new MockHost());
|
|
|
|
var extDir = Path.Combine(_tempDir, "profile_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Profile Ext\",\"version\":\"1.0\"}");
|
|
|
|
await mgrA.InstallFromFolderAsync(extDir);
|
|
Assert.Single(mgrA.Extensions);
|
|
Assert.Empty(mgrB.Extensions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test098_E2E_Disabled_Extension_ContentScript_Suppression()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "disabled_cs");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "cs_suppress");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Suppress\",\"version\":\"1.0\"}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
await mgr.SetEnabledAsync(item!.Id, false);
|
|
Assert.False(item.IsEnabled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Test099_E2E_VirtualHost_All_MimeTypes()
|
|
{
|
|
var exts = new[] { ".html", ".js", ".css", ".json", ".png", ".jpg", ".svg", ".woff2", ".wasm" };
|
|
foreach (var ext in exts)
|
|
{
|
|
var mime = ext switch
|
|
{
|
|
".html" => "text/html; charset=utf-8",
|
|
".js" => "application/javascript; charset=utf-8",
|
|
".css" => "text/css; charset=utf-8",
|
|
".json" => "application/json; charset=utf-8",
|
|
".png" => "image/png",
|
|
".jpg" => "image/jpeg",
|
|
".svg" => "image/svg+xml",
|
|
".woff2" => "font/woff2",
|
|
_ => "application/octet-stream"
|
|
};
|
|
Assert.NotEmpty(mime);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Test100_E2E_WebStore_Url_Recognition()
|
|
{
|
|
var urls = new[]
|
|
{
|
|
"https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
|
"https://chrome.google.com/webstore/detail/dark-reader/eimadpbcbfnmbkopoojfekhnkhdbieeh",
|
|
"https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkmingmknioojfogehpchnflpk?hl=en"
|
|
};
|
|
foreach (var u in urls)
|
|
{
|
|
var id = ChromeWebStoreClient.ExtractExtensionId(u);
|
|
Assert.NotNull(id);
|
|
Assert.Equal(32, id.Length);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test101_E2E_MegaStress_50Extensions_Batch_Operations()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "mega_50");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var extDir = Path.Combine(_tempDir, $"mega_{i}");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), $"{{\"name\":\"Mega Ext {i}\",\"version\":\"{i}.0\"}}");
|
|
var item = await mgr.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
}
|
|
|
|
Assert.Equal(50, mgr.Extensions.Count);
|
|
|
|
// 일괄 핀 고정
|
|
foreach (var ext in mgr.Extensions.Take(10))
|
|
{
|
|
await mgr.SetPinnedAsync(ext.Id, true);
|
|
Assert.True(ext.IsPinned);
|
|
}
|
|
|
|
// 일괄 재로드 검증
|
|
var mgrReload = new ExtensionManager(dataDir, new MockHost());
|
|
await mgrReload.LoadExtensionsAsync();
|
|
Assert.Equal(50, mgrReload.Extensions.Count);
|
|
Assert.Equal(10, mgrReload.Extensions.Count(x => x.IsPinned));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test102_E2E_IconResolver_Fallback_All_Types()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "icon_fallback");
|
|
Directory.CreateDirectory(Path.Combine(extDir, "icons"));
|
|
File.WriteAllText(Path.Combine(extDir, "icons", "icon_128.png"), "dummy png");
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Icon Test\",\"version\":\"1.0\"}");
|
|
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "icon_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.NotNull(item.IconPath);
|
|
Assert.EndsWith("icon_128.png", item.IconPath);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test103_E2E_Locale_Missing_Fallback_To_Default_Locale()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "def_loc");
|
|
var locEn = Path.Combine(extDir, "_locales", "en");
|
|
Directory.CreateDirectory(locEn);
|
|
File.WriteAllText(Path.Combine(locEn, "messages.json"), "{\"extName\":{\"message\":\"English Name\"}}");
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "__MSG_extName__",
|
|
"version": "1.0",
|
|
"default_locale": "en"
|
|
}
|
|
""");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "def_loc_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.Equal("English Name", item.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test104_E2E_Locale_Case_Insensitive_Key_Matching()
|
|
{
|
|
var extDir = Path.Combine(_tempDir, "case_loc");
|
|
var locKo = Path.Combine(extDir, "_locales", "ko");
|
|
Directory.CreateDirectory(locKo);
|
|
File.WriteAllText(Path.Combine(locKo, "messages.json"), "{\"EXTNAME\":{\"message\":\"대소문자 무관\"}}");
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), """
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "__MSG_extname__",
|
|
"version": "1.0",
|
|
"default_locale": "ko"
|
|
}
|
|
""");
|
|
var mgr = new ExtensionManager(Path.Combine(_tempDir, "case_loc_data"), new MockHost());
|
|
var item = await mgr.InstallFromFolderAsync(extDir);
|
|
Assert.NotNull(item);
|
|
Assert.Equal("대소문자 무관", item.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test105_E2E_Full_Lifecycle_Install_Toggle_Uninstall_Cycle()
|
|
{
|
|
var dataDir = Path.Combine(_tempDir, "cycle_data");
|
|
var mgr = new ExtensionManager(dataDir, new MockHost());
|
|
var extDir = Path.Combine(_tempDir, "cycle_ext");
|
|
Directory.CreateDirectory(extDir);
|
|
File.WriteAllText(Path.Combine(extDir, "manifest.json"), "{\"name\":\"Cycle Ext\",\"version\":\"1.0\"}");
|
|
|
|
// 1. Install
|
|
var item = await mgr.InstallFromFolderAsync(extDir, copyToStorage: true);
|
|
Assert.NotNull(item);
|
|
Assert.Single(mgr.Extensions);
|
|
|
|
// 2. Pin
|
|
await mgr.SetPinnedAsync(item.Id, true);
|
|
Assert.True(item.IsPinned);
|
|
|
|
// 3. Disable
|
|
await mgr.SetEnabledAsync(item.Id, false);
|
|
Assert.False(item.IsEnabled);
|
|
|
|
// 4. Enable
|
|
await mgr.SetEnabledAsync(item.Id, true);
|
|
Assert.True(item.IsEnabled);
|
|
|
|
// 5. Uninstall
|
|
var ok = await mgr.UninstallAsync(item.Id);
|
|
Assert.True(ok);
|
|
Assert.Empty(mgr.Extensions);
|
|
}
|
|
|
|
private sealed class MockHost : IBrowserExtensionHost
|
|
{
|
|
public Task<string?> AddExtensionAsync(string folderPath, string extensionId) => Task.FromResult<string?>(extensionId);
|
|
public Task<bool> RemoveExtensionAsync(string extensionId) => Task.FromResult(true);
|
|
public Task<bool> SetExtensionEnabledAsync(string extensionId, bool enabled) => Task.FromResult(true);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Phase 60: Hyper-Extension Subsystem 전방위 검증 테스트 스위트.
|
|
/// DeclarativeNetRequest(DNR) 룰 매칭, chrome.alarms 스케줄러, UserScript GM 엔진, Permission Auditor를 검증합니다.
|
|
/// </summary>
|
|
public sealed class HyperExtensionSubsystemPhase60Tests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
|
|
public HyperExtensionSubsystemPhase60Tests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), $"paca_hyperext_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(_tempDir))
|
|
Directory.Delete(_tempDir, true);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
#region 1. DeclarativeNetRequest Engine Tests
|
|
|
|
[Fact]
|
|
public void DnrEngine_기본_블록_룰이_정확히_차단된다()
|
|
{
|
|
var engine = new DeclarativeNetRequestEngine();
|
|
var rules = new List<DnrRule>
|
|
{
|
|
new()
|
|
{
|
|
Id = 1,
|
|
Priority = 1,
|
|
Action = new DnrRuleAction { Type = "block" },
|
|
Condition = new DnrRuleCondition { UrlFilter = "||ads.tracker.com^" }
|
|
}
|
|
};
|
|
|
|
engine.RegisterStaticRules("ext_ublock", rules);
|
|
Assert.Equal(1, engine.TotalRuleCount);
|
|
|
|
var eval = engine.EvaluateRequest("https://ads.tracker.com/pixel.gif");
|
|
Assert.True(eval.IsMatched);
|
|
Assert.Equal(DnrRuleActionType.Block, eval.ActionType);
|
|
Assert.Equal(1, eval.MatchedRuleId);
|
|
Assert.Equal("ext_ublock", eval.MatchedExtensionId);
|
|
|
|
var allowEval = engine.EvaluateRequest("https://safe.site.org/index.html");
|
|
Assert.False(allowEval.IsMatched);
|
|
Assert.Equal(DnrRuleActionType.Allow, allowEval.ActionType);
|
|
}
|
|
|
|
[Fact]
|
|
public void DnrEngine_접두사_접미사_와일드카드_필터_매칭()
|
|
{
|
|
var engine = new DeclarativeNetRequestEngine();
|
|
var rules = new List<DnrRule>
|
|
{
|
|
new()
|
|
{
|
|
Id = 1,
|
|
Priority = 1,
|
|
Action = new DnrRuleAction { Type = "block" },
|
|
Condition = new DnrRuleCondition { UrlFilter = "|http://insecure.com" }
|
|
},
|
|
new()
|
|
{
|
|
Id = 2,
|
|
Priority = 1,
|
|
Action = new DnrRuleAction { Type = "block" },
|
|
Condition = new DnrRuleCondition { UrlFilter = ".mp4|" }
|
|
},
|
|
new()
|
|
{
|
|
Id = 3,
|
|
Priority = 1,
|
|
Action = new DnrRuleAction { Type = "block" },
|
|
Condition = new DnrRuleCondition { UrlFilter = "*banner*ad*" }
|
|
}
|
|
};
|
|
|
|
engine.RegisterStaticRules("ext1", rules);
|
|
|
|
// Prefix
|
|
Assert.True(engine.EvaluateRequest("http://insecure.com/api").IsMatched);
|
|
Assert.False(engine.EvaluateRequest("https://insecure.com/api").IsMatched);
|
|
|
|
// Suffix
|
|
Assert.True(engine.EvaluateRequest("https://video.cdn.com/stream/file.mp4").IsMatched);
|
|
Assert.False(engine.EvaluateRequest("https://video.cdn.com/stream/file.mp4?token=123").IsMatched);
|
|
|
|
// Wildcard
|
|
Assert.True(engine.EvaluateRequest("https://site.com/static/banner_wide_ad_728x90.png").IsMatched);
|
|
}
|
|
|
|
[Fact]
|
|
public void DnrEngine_정규식_필터_및_리다이렉트_변환_검증()
|
|
{
|
|
var engine = new DeclarativeNetRequestEngine();
|
|
var rules = new List<DnrRule>
|
|
{
|
|
new()
|
|
{
|
|
Id = 10,
|
|
Priority = 5,
|
|
Action = new DnrRuleAction
|
|
{
|
|
Type = "redirect",
|
|
Redirect = new DnrRedirectOptions
|
|
{
|
|
Transform = new DnrUrlTransform { Scheme = "https", Host = "redirected.secure.com" }
|
|
}
|
|
},
|
|
Condition = new DnrRuleCondition
|
|
{
|
|
RegexFilter = @"^http://old\.service\.com/(.*)$"
|
|
}
|
|
}
|
|
};
|
|
|
|
engine.RegisterStaticRules("ext_redirect", rules);
|
|
|
|
var eval = engine.EvaluateRequest("http://old.service.com/dashboard/home");
|
|
Assert.True(eval.IsMatched);
|
|
Assert.Equal(DnrRuleActionType.Redirect, eval.ActionType);
|
|
Assert.Equal("https://redirected.secure.com/dashboard/home", eval.RedirectUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public void DnrEngine_동적_및_세션_룰_우선순위_평가_검증()
|
|
{
|
|
var engine = new DeclarativeNetRequestEngine();
|
|
|
|
// 1. Static Rule (Priority 1) -> Block
|
|
engine.RegisterStaticRules("ext_test", new[]
|
|
{
|
|
new DnrRule { Id = 1, Priority = 1, Action = new DnrRuleAction { Type = "block" }, Condition = new DnrRuleCondition { UrlFilter = "analytics.js" } }
|
|
});
|
|
|
|
// 2. Dynamic Rule (Priority 10) -> Allow
|
|
engine.UpdateDynamicRules("ext_test", null, new[]
|
|
{
|
|
new DnrRule { Id = 2, Priority = 10, Action = new DnrRuleAction { Type = "allow" }, Condition = new DnrRuleCondition { UrlFilter = "analytics.js" } }
|
|
});
|
|
|
|
// Dynamic Rule이 Static보다 우선순위가 높아 Allow 되어야 함
|
|
var eval = engine.EvaluateRequest("https://site.com/js/analytics.js");
|
|
Assert.True(eval.IsMatched);
|
|
Assert.Equal(DnrRuleActionType.Allow, eval.ActionType);
|
|
Assert.Equal(2, eval.MatchedRuleId);
|
|
|
|
// Dynamic Rule 삭제 시 다시 Static Rule(Block)로 복귀
|
|
engine.UpdateDynamicRules("ext_test", new[] { 2 }, null);
|
|
var evalAfterRemove = engine.EvaluateRequest("https://site.com/js/analytics.js");
|
|
Assert.True(evalAfterRemove.IsMatched);
|
|
Assert.Equal(DnrRuleActionType.Block, evalAfterRemove.ActionType);
|
|
Assert.Equal(1, evalAfterRemove.MatchedRuleId);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 2. Extension Alarm Manager Tests
|
|
|
|
[Fact]
|
|
public async Task AlarmManager_생성_조회_단발성_이벤트_발송_검증()
|
|
{
|
|
using var alarmMgr = new ExtensionAlarmManager();
|
|
string triggeredExt = "";
|
|
string triggeredName = "";
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
|
|
alarmMgr.OnAlarmTriggered += (extId, alarm) =>
|
|
{
|
|
triggeredExt = extId;
|
|
triggeredName = alarm.Name;
|
|
tcs.TrySetResult(true);
|
|
};
|
|
|
|
// 50ms 후 트리거
|
|
alarmMgr.Create("ext_test", "quick_alarm", new AlarmCreateInfo
|
|
{
|
|
When = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 50
|
|
});
|
|
|
|
var alarm = alarmMgr.Get("ext_test", "quick_alarm");
|
|
Assert.NotNull(alarm);
|
|
Assert.Equal("quick_alarm", alarm.Name);
|
|
|
|
var completed = await Task.WhenAny(tcs.Task, Task.Delay(5000));
|
|
Assert.Equal(tcs.Task, completed);
|
|
Assert.Equal("ext_test", triggeredExt);
|
|
Assert.Equal("quick_alarm", triggeredName);
|
|
|
|
// 트리거 후 단발 알람 자동 제거 확인
|
|
Assert.Null(alarmMgr.Get("ext_test", "quick_alarm"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AlarmManager_알람_취소_및_전체_삭제()
|
|
{
|
|
using var alarmMgr = new ExtensionAlarmManager();
|
|
alarmMgr.Create("ext_test", "alarm1", new AlarmCreateInfo { DelayInMinutes = 10 });
|
|
alarmMgr.Create("ext_test", "alarm2", new AlarmCreateInfo { DelayInMinutes = 20 });
|
|
|
|
Assert.Equal(2, alarmMgr.GetAll("ext_test").Count);
|
|
|
|
var cleared = alarmMgr.Clear("ext_test", "alarm1");
|
|
Assert.True(cleared);
|
|
Assert.Single(alarmMgr.GetAll("ext_test"));
|
|
|
|
alarmMgr.ClearAll("ext_test");
|
|
Assert.Empty(alarmMgr.GetAll("ext_test"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 3. UserScript Engine Tests
|
|
|
|
[Fact]
|
|
public void UserScriptEngine_메타데이터_파싱_및_URL_매칭_검증()
|
|
{
|
|
var scriptSource = """
|
|
// ==UserScript==
|
|
// @name YouTube Ad Skipper Pro
|
|
// @namespace http://tampermonkey.net/
|
|
// @version 2.5.0
|
|
// @description Automatically skips ads on YouTube
|
|
// @author Paca Developer
|
|
// @match *://*.youtube.com/*
|
|
// @exclude *://*.youtube.com/embed/*
|
|
// @grant GM_getValue
|
|
// @grant GM_setValue
|
|
// @grant GM_addStyle
|
|
// @run-at document-start
|
|
// ==/UserScript==
|
|
|
|
console.log("Ad skipper active!");
|
|
""";
|
|
|
|
var meta = UserScriptEngine.ParseMetadata(scriptSource);
|
|
Assert.Equal("YouTube Ad Skipper Pro", meta.Name);
|
|
Assert.Equal("2.5.0", meta.Version);
|
|
Assert.Equal("document-start", meta.RunAt);
|
|
Assert.Single(meta.MatchPatterns);
|
|
Assert.Single(meta.ExcludePatterns);
|
|
Assert.Equal(3, meta.Grants.Count);
|
|
|
|
// URL Matching
|
|
Assert.True(UserScriptEngine.IsUrlMatched(meta, "https://www.youtube.com/watch?v=12345"));
|
|
Assert.False(UserScriptEngine.IsUrlMatched(meta, "https://www.youtube.com/embed/12345"));
|
|
Assert.False(UserScriptEngine.IsUrlMatched(meta, "https://vimeo.com/12345"));
|
|
}
|
|
|
|
[Fact]
|
|
public void UserScriptEngine_스크립트_등록_및_GM_폴리필_코드_빌드_검증()
|
|
{
|
|
var engine = new UserScriptEngine();
|
|
var script = engine.RegisterScript("""
|
|
// ==UserScript==
|
|
// @name Dark Mode Injector
|
|
// @match https://news.ycombinator.com/*
|
|
// @grant GM_addStyle
|
|
// ==/UserScript==
|
|
|
|
GM_addStyle("body { background-color: #111 !important; color: #eee !important; }");
|
|
""");
|
|
|
|
Assert.Equal(1, engine.ScriptCount);
|
|
var matches = engine.GetMatchingScripts("https://news.ycombinator.com/news", "document-idle");
|
|
Assert.Single(matches);
|
|
|
|
var bundle = UserScriptEngine.BuildInjectedCode(script);
|
|
Assert.Contains("GM_info", bundle);
|
|
Assert.Contains("GM_addStyle", bundle);
|
|
Assert.Contains("GM_getValue", bundle);
|
|
Assert.Contains("Dark Mode Injector", bundle);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 4. Extension Permission Auditor Tests
|
|
|
|
[Fact]
|
|
public void PermissionAuditor_고위험_권한_및_위험_조합_감사_검증()
|
|
{
|
|
var manifest = new ExtensionManifest
|
|
{
|
|
Name = "Dangerous Ext",
|
|
Permissions = new List<string> { "webRequestBlocking", "cookies", "tabs" },
|
|
HostPermissions = new List<string> { "<all_urls>" }
|
|
};
|
|
|
|
var report = ExtensionPermissionAuditor.Audit(manifest, "ext_danger");
|
|
Assert.Equal("Dangerous Ext", report.ExtensionName);
|
|
Assert.True(report.RiskScore >= 70, $"위험 점수({report.RiskScore})가 70점 이상이어야 합니다.");
|
|
Assert.Equal(PermissionRiskLevel.Critical, report.OverallRisk);
|
|
Assert.NotEmpty(report.DangerousCombinations);
|
|
Assert.Contains(report.DangerousCombinations, c => c.Contains("webRequestBlocking"));
|
|
Assert.Contains(report.DangerousCombinations, c => c.Contains("쿠키"));
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionAuditor_저위험_선언형_권한_안전_평가_검증()
|
|
{
|
|
var manifest = new ExtensionManifest
|
|
{
|
|
Name = "Safe Adblocker",
|
|
Permissions = new List<string> { "declarativeNetRequest", "storage", "alarms" }
|
|
};
|
|
|
|
var report = ExtensionPermissionAuditor.Audit(manifest, "ext_safe");
|
|
Assert.Equal(PermissionRiskLevel.Low, report.OverallRisk);
|
|
Assert.True(report.RiskScore < 20);
|
|
Assert.Empty(report.DangerousCombinations);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bitwarden_Polyfill_DistinctStorageAreas_Session_Local_Sync_Managed()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "bw_storage"));
|
|
var poly = storage.GeneratePolyfillScript("nngceckbapebfimnlniiiahkandclblb");
|
|
|
|
Assert.Contains("chrome.storage.local = createStorageArea('local')", poly);
|
|
Assert.Contains("chrome.storage.session = createStorageArea('session')", poly);
|
|
Assert.Contains("chrome.storage.sync = createStorageArea('sync')", poly);
|
|
Assert.Contains("chrome.storage.managed = createStorageArea('managed')", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bitwarden_Polyfill_PortMessageBridge_InitializationAndForegroundSaveGet()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "bw_port"));
|
|
var poly = storage.GeneratePolyfillScript("nngceckbapebfimnlniiiahkandclblb");
|
|
|
|
Assert.Contains("originator: 'background'", poly);
|
|
Assert.Contains("action: 'initialization'", poly);
|
|
Assert.Contains("action: 'subject_update'", poly);
|
|
Assert.Contains("message.originator === 'foreground'", poly);
|
|
Assert.Contains("message.action === 'get'", poly);
|
|
Assert.Contains("message.action === 'save'", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bitwarden_Polyfill_RuntimeMessages_TriageAndFido2_ResolveSynchronously()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "bw_msg"));
|
|
var poly = storage.GeneratePolyfillScript("nngceckbapebfimnlniiiahkandclblb");
|
|
|
|
Assert.Contains("getAutofillTriageResult", poly);
|
|
Assert.Contains("BrowserFido2UserInterfaceServiceMessage", poly);
|
|
Assert.Contains("clearClipboard", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bitwarden_Polyfill_AuxiliaryApis_Windows_Contexts_Permissions_Offscreen_SidePanel()
|
|
{
|
|
var storage = new ExtensionStorageService(Path.Combine(_tempDir, "bw_apis"));
|
|
var poly = storage.GeneratePolyfillScript("nngceckbapebfimnlniiiahkandclblb");
|
|
|
|
Assert.Contains("chrome.windows.getCurrent", poly);
|
|
Assert.Contains("chrome.runtime.getContexts", poly);
|
|
Assert.Contains("chrome.offscreen.createDocument", poly);
|
|
Assert.Contains("chrome.sidePanel.setOptions", poly);
|
|
Assert.Contains("chrome.extension.getViews", poly);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtensionManager_GetPopupUrl_ReturnsValidVirtualHostUrl()
|
|
{
|
|
var item = new ExtensionItem
|
|
{
|
|
Id = "nngceckbapebfimnlniiiahkandclblb",
|
|
PopupPath = "popup/index.html"
|
|
};
|
|
var manager = new ExtensionManager(_tempDir, new MockHost());
|
|
var url = manager.GetPopupUrl(item);
|
|
|
|
Assert.NotNull(url);
|
|
Assert.Equal("https://ext-nngceckbapebfimnlniiiahkandclblb.local/popup/index.html", url);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExtensionManager_ConcurrentSnapshot_And_Mutation_IsThreadSafe()
|
|
{
|
|
// GetExtensionsSnapshot 열거 도중 비동기 확장이 추가/제거될 때 Collection was modified 충돌이 발생하지 않음을 기계적 검증
|
|
var manager = new ExtensionManager(_tempDir, new MockHost());
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
manager.Extensions.Add(new ExtensionItem { Id = $"ext_{i}", Name = $"Ext {i}", IsEnabled = true });
|
|
}
|
|
|
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
|
var readerTask = Task.Run(() =>
|
|
{
|
|
while (!cts.Token.IsCancellationRequested)
|
|
{
|
|
var snapshot = manager.GetExtensionsSnapshot();
|
|
var active = snapshot.Where(x => x.IsEnabled).ToList();
|
|
Assert.NotNull(active);
|
|
}
|
|
});
|
|
|
|
var writerTask = Task.Run(() =>
|
|
{
|
|
int counter = 100;
|
|
while (!cts.Token.IsCancellationRequested)
|
|
{
|
|
var item = new ExtensionItem { Id = $"ext_dynamic_{counter++}", Name = "Dynamic", IsEnabled = true };
|
|
manager.AddExtensionSafe(item);
|
|
if (manager.GetExtensionsSnapshot().Count > 100)
|
|
{
|
|
manager.RemoveExtensionSafe(item);
|
|
}
|
|
}
|
|
});
|
|
|
|
await Task.WhenAll(readerTask, writerTask);
|
|
}
|
|
|
|
private sealed class MockHost : IBrowserExtensionHost
|
|
{
|
|
public Task<string?> AddExtensionAsync(string folderPath, string extensionId) => Task.FromResult<string?>(extensionId);
|
|
public Task<bool> RemoveExtensionAsync(string extensionId) => Task.FromResult(true);
|
|
public Task<bool> SetExtensionEnabledAsync(string extensionId, bool enabled) => Task.FromResult(true);
|
|
}
|
|
|
|
#endregion
|
|
}
|