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:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,532 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Paca.Browser.Url;
using Xunit;
namespace Paca.Tests;
public sealed class ModernBrowsingMilestone11Phase111Tests : IDisposable
{
private readonly string _tempDir;
public ModernBrowsingMilestone11Phase111Tests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m11_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
}
catch { }
}
[Fact]
public void Gate175_CleanLinkExtractor_StripsTrackingParamsAndUnpacksAmpUrls()
{
// Arrange
var extractor = new CleanLinkExtractor();
// Act 1: Strip UTM and social tracking parameters while keeping content params
var rawSocialUrl = "https://example.com/article?utm_source=twitter&utm_medium=social&utm_campaign=autumn_sale&id=456&utm_content=logolink";
var cleanedResult = extractor.Clean(rawSocialUrl);
// Assert 1
Assert.True(cleanedResult.WasModified);
Assert.False(cleanedResult.WasAmpUnpacked);
Assert.Equal("https://example.com/article?id=456", cleanedResult.CleanedUrl);
Assert.Contains("utm_source", cleanedResult.StrippedParameters);
Assert.Contains("utm_medium", cleanedResult.StrippedParameters);
Assert.Contains("utm_campaign", cleanedResult.StrippedParameters);
Assert.Contains("utm_content", cleanedResult.StrippedParameters);
Assert.DoesNotContain("id", cleanedResult.StrippedParameters);
// Act 2: Strip multiple ad tracker params (fbclid, gclid, msclkid, mc_cid)
var rawAdUrl = "https://shop.example.com/item/shoes?fbclid=IwAR123&gclid=Cj0K456&msclkid=789&mc_cid=abc&mc_eid=def";
var adResult = extractor.Clean(rawAdUrl);
// Assert 2: All query params were tracking, so clean URL has no query string
Assert.True(adResult.WasModified);
Assert.Equal("https://shop.example.com/item/shoes", adResult.CleanedUrl);
Assert.Equal(5, adResult.StrippedParameters.Count);
// Act 3: Preserve URL fragment/hash
var rawWithHash = "https://example.com/docs?utm_source=newsletter#section-installation";
var hashResult = extractor.Clean(rawWithHash);
Assert.Equal("https://example.com/docs#section-installation", hashResult.CleanedUrl);
// Act 4: Unpack Google AMP URLs to canonical destination
var googleAmpHttps = "https://www.google.com/amp/s/news.example.com/politics/2026-election";
var unpackedResult = extractor.Clean(googleAmpHttps);
Assert.True(unpackedResult.WasModified);
Assert.True(unpackedResult.WasAmpUnpacked);
Assert.Equal("https://news.example.com/politics/2026-election", unpackedResult.CleanedUrl);
Assert.Equal("https://news.example.com/politics/2026-election", unpackedResult.UnpackedCanonicalUrl);
// Act 5: Unpack AMP cache CDN and simultaneously strip tracking params
var ampCdnWithTracking = "https://example-com.cdn.ampproject.org/c/s/example.com/story?utm_source=amp_share&fbclid=xyz&page=2";
var combinedResult = extractor.Clean(ampCdnWithTracking);
Assert.True(combinedResult.WasModified);
Assert.True(combinedResult.WasAmpUnpacked);
Assert.Equal("https://example.com/story?page=2", combinedResult.CleanedUrl);
Assert.Contains("utm_source", combinedResult.StrippedParameters);
Assert.Contains("fbclid", combinedResult.StrippedParameters);
// Act 6: Clean non-tracking URL returns untouched
var pristineUrl = "https://clean.example.org/search?q=csharp&page=1";
var pristineResult = extractor.Clean(pristineUrl);
Assert.False(pristineResult.WasModified);
Assert.Equal(pristineUrl, pristineResult.CleanedUrl);
Assert.Empty(pristineResult.StrippedParameters);
// Act 7: Custom tracking parameters support
var customExtractor = new CleanLinkExtractor(new[] { "custom_trk", "affiliate_tracker" });
var customUrl = "https://store.example.com/book?custom_trk=99&title=tales";
var customResult = customExtractor.Clean(customUrl);
Assert.Equal("https://store.example.com/book?title=tales", customResult.CleanedUrl);
Assert.Contains("custom_trk", customResult.StrippedParameters);
}
[Fact]
public void Gate176_TextFragmentDeepLinker_GeneratesAndParsesTextFragments()
{
// Arrange
var linker = new TextFragmentDeepLinker();
// Act 1: Generate simple text fragment
var simpleLink = linker.GenerateLink("https://en.wikipedia.org/wiki/Browser", "Web browser");
Assert.Equal("https://en.wikipedia.org/wiki/Browser#:~:text=Web%20browser", simpleLink);
// Act 2: Generate text fragment with start and end range
var rangeLink = linker.GenerateLink("https://example.com/doc", "Getting Started", "Next Steps");
Assert.Equal("https://example.com/doc#:~:text=Getting%20Started,Next%20Steps", rangeLink);
// Act 3: Generate text fragment with context prefix and suffix
var contextLink = linker.GenerateLink(
"https://example.com/manual",
textStart: "Configure PACA",
textEnd: "Restart application",
prefix: "Step 1 -",
suffix: "- Complete");
Assert.Contains("#:~:text=", contextLink);
Assert.Contains("Step%201%20--", contextLink);
Assert.Contains("Configure%20PACA,Restart%20application", contextLink);
Assert.Contains("--%20Complete", contextLink);
// Act 4: Generate link preserving existing fragment hash
var hashBase = "https://example.com/terms#privacy";
var hashLink = linker.GenerateLink(hashBase, "Data Collection");
Assert.Equal("https://example.com/terms#privacy:~:text=Data%20Collection", hashLink);
// Act 5: Parse multi-directive text fragment URL
var complexUrl = "https://example.com/article#section1:~:text=Important%20notice&text=prefix-,highlight%20start,highlight%20end,-suffix";
var parsed = linker.Parse(complexUrl);
Assert.True(parsed.HasTextFragment);
Assert.Equal("https://example.com/article#section1", parsed.BaseUrlWithoutDirective);
Assert.Equal("section1", parsed.TraditionalFragment);
Assert.Equal(2, parsed.Directives.Count);
// Directive 1
Assert.Equal("Important notice", parsed.Directives[0].TextStart);
Assert.Null(parsed.Directives[0].TextEnd);
Assert.Null(parsed.Directives[0].Prefix);
Assert.Null(parsed.Directives[0].Suffix);
// Directive 2
Assert.Equal("prefix", parsed.Directives[1].Prefix);
Assert.Equal("highlight start", parsed.Directives[1].TextStart);
Assert.Equal("highlight end", parsed.Directives[1].TextEnd);
Assert.Equal("suffix", parsed.Directives[1].Suffix);
// Act 6: Strip text fragment directive, preserving traditional hash
var strippedWithHash = linker.RemoveTextFragments("https://example.com/doc#summary:~:text=Summary%20Text");
Assert.Equal("https://example.com/doc#summary", strippedWithHash);
var strippedWithoutHash = linker.RemoveTextFragments("https://example.com/doc#:~:text=Alone");
Assert.Equal("https://example.com/doc", strippedWithoutHash);
// Act 7: Generate DOM highlight script
var highlightScript = linker.GenerateHighlightScript(parsed.Directives);
Assert.Contains("window.find", highlightScript);
Assert.Contains("Important notice", highlightScript);
Assert.Contains("highlight start", highlightScript);
}
[Fact]
public void Gate177_UrlBreadcrumbNavigator_SplitsHierarchyAndGeneratesBreadcrumbs()
{
// Arrange
var navigator = new UrlBreadcrumbNavigator();
// Act 1: Parse hierarchical URL
var rawUrl = "https://github.com/dotnet/runtime/tree/main/src/libraries";
var breadcrumbs = navigator.ParseBreadcrumbs(rawUrl);
// Assert 1: 7 levels of breadcrumbs
Assert.Equal(7, breadcrumbs.Count);
Assert.Equal("github.com", breadcrumbs[0].Label);
Assert.Equal("https://github.com", breadcrumbs[0].Url);
Assert.Equal(0, breadcrumbs[0].Level);
Assert.True(breadcrumbs[0].IsHost);
Assert.False(breadcrumbs[0].IsCurrent);
Assert.Equal("dotnet", breadcrumbs[1].Label);
Assert.Equal("https://github.com/dotnet", breadcrumbs[1].Url);
Assert.Equal(1, breadcrumbs[1].Level);
Assert.False(breadcrumbs[1].IsHost);
Assert.Equal("runtime", breadcrumbs[2].Label);
Assert.Equal("https://github.com/dotnet/runtime", breadcrumbs[2].Url);
Assert.Equal("tree", breadcrumbs[3].Label);
Assert.Equal("https://github.com/dotnet/runtime/tree", breadcrumbs[3].Url);
Assert.Equal("main", breadcrumbs[4].Label);
Assert.Equal("https://github.com/dotnet/runtime/tree/main", breadcrumbs[4].Url);
Assert.Equal("src", breadcrumbs[5].Label);
Assert.Equal("https://github.com/dotnet/runtime/tree/main/src", breadcrumbs[5].Url);
Assert.Equal("libraries", breadcrumbs[6].Label);
Assert.Equal("https://github.com/dotnet/runtime/tree/main/src/libraries", breadcrumbs[6].Url);
Assert.Equal(6, breadcrumbs[6].Level);
Assert.True(breadcrumbs[6].IsCurrent);
// Act 2: Root URL breadcrumb
var rootBreadcrumbs = navigator.ParseBreadcrumbs("https://paca.browser.dev/");
Assert.Single(rootBreadcrumbs);
Assert.Equal("paca.browser.dev", rootBreadcrumbs[0].Label);
Assert.True(rootBreadcrumbs[0].IsHost);
Assert.True(rootBreadcrumbs[0].IsCurrent);
// Act 3: Parent URL navigation
var parentUrl = navigator.GetParentUrl("https://github.com/dotnet/runtime/tree");
Assert.Equal("https://github.com/dotnet/runtime", parentUrl);
var topParentUrl = navigator.GetParentUrl("https://github.com/dotnet");
Assert.Equal("https://github.com", topParentUrl);
var rootParentUrl = navigator.GetParentUrl("https://github.com");
Assert.Null(rootParentUrl);
// Act 4: Ancestor URLs collection
var ancestors = navigator.GetAncestorUrls("https://github.com/dotnet/runtime");
Assert.Equal(2, ancestors.Count);
Assert.Equal("https://github.com", ancestors[0]);
Assert.Equal("https://github.com/dotnet", ancestors[1]);
// Act 5: Percent-encoded segment decoding in labels
var encodedUrl = "https://example.org/files/My%20Project%20Files/Reports%26Docs";
var encodedCrumbs = navigator.ParseBreadcrumbs(encodedUrl);
Assert.Equal(4, encodedCrumbs.Count);
Assert.Equal("My Project Files", encodedCrumbs[2].Label);
Assert.Equal("Reports&Docs", encodedCrumbs[3].Label);
Assert.Equal("https://example.org/files/My%20Project%20Files", encodedCrumbs[2].Url);
}
[Fact]
public void Gate178_OmniboxQrCoordinator_GeneratesSvgQrCodeAndMobileHandoffLinks()
{
// Arrange
var coordinator = new OmniboxQrCoordinator();
// Act 1: Generate valid QR code SVG payload
var testUrl = "https://github.com/paca-browser/desktop";
var svg = coordinator.GenerateQrSvg(testUrl, moduleSize: 8);
// Assert 1
Assert.False(string.IsNullOrWhiteSpace(svg));
Assert.Contains("<svg", svg, StringComparison.OrdinalIgnoreCase);
Assert.Contains("http://www.w3.org/2000/svg", svg);
Assert.Contains("</svg>", svg, StringComparison.OrdinalIgnoreCase);
// Act 2: Custom theme styling (Dark mode SVG)
var darkSvg = coordinator.GenerateQrSvg(
testUrl,
moduleSize: 10,
foregroundColorHex: "#CDD6F4",
backgroundColorHex: "#1E1E2E");
Assert.Contains("#CDD6F4", darkSvg);
Assert.Contains("#1E1E2E", darkSvg);
// Act 3: Mobile handoff payload generation
var handoff = coordinator.CreateHandoffPayload(
"https://news.ycombinator.com/item?id=12345",
title: "Hacker News Story",
deviceId: "desktop-paca-win");
// Assert 3
Assert.Equal("https://news.ycombinator.com/item?id=12345", handoff.TargetUrl);
Assert.Equal("Hacker News Story", handoff.Title);
Assert.Equal("desktop-paca-win", handoff.DeviceId);
Assert.StartsWith("paca://handoff?", handoff.HandoffDeepLink);
Assert.Contains("url=" + Uri.EscapeDataString("https://news.ycombinator.com/item?id=12345"), handoff.HandoffDeepLink);
Assert.Contains("<svg", handoff.QrCodeSvg);
Assert.True(handoff.Timestamp <= DateTimeOffset.UtcNow);
// Act 4: Parse back mobile handoff deep link
var parseSuccess = coordinator.TryParseHandoffLink(
handoff.HandoffDeepLink,
out var parsedUrl,
out var parsedTitle,
out var parsedDeviceId);
Assert.True(parseSuccess);
Assert.Equal(handoff.TargetUrl, parsedUrl);
Assert.Equal(handoff.Title, parsedTitle);
Assert.Equal(handoff.DeviceId, parsedDeviceId);
// Act 5: Validate URL
Assert.True(coordinator.ValidateUrl("https://example.com"));
Assert.True(coordinator.ValidateUrl("http://localhost:5000/api"));
Assert.False(coordinator.ValidateUrl("invalid-not-a-url"));
Assert.False(coordinator.ValidateUrl(""));
Assert.Throws<ArgumentException>(() => coordinator.GenerateQrSvg("not a valid url"));
}
[Fact]
public async Task Gate179_QuickDictionaryLookup_ResolvesWordDefinitionsAndTranslations()
{
// Arrange
var lookup = new QuickDictionaryLookup();
// Act 1: Text sanitization (double-click trims punctuation, quotes, whitespace)
var sanitized = lookup.SanitizeWord(" \"omnibox,\" ");
Assert.Equal("omnibox", sanitized);
var hyphenated = lookup.SanitizeWord("(browser-engine!)");
Assert.Equal("browser-engine", hyphenated);
// Act 2: Built-in vocabulary lookup (English -> Korean translation)
var omniboxResult = lookup.Lookup("omnibox", targetLanguage: "ko");
// Assert 2
Assert.True(omniboxResult.Found);
Assert.Equal("omnibox", omniboxResult.Word);
Assert.NotNull(omniboxResult.Phonetic);
Assert.NotEmpty(omniboxResult.Meanings);
Assert.Contains(omniboxResult.Meanings, m => m.PartOfSpeech.Equals("noun", StringComparison.OrdinalIgnoreCase));
Assert.True(omniboxResult.Translations.ContainsKey("ko"));
Assert.Contains("주소창", omniboxResult.Translations["ko"]);
// Act 3: Lookup standard word with definitions and sample sentences
var browserResult = lookup.Lookup("browser", targetLanguage: "ko");
Assert.True(browserResult.Found);
Assert.NotNull(browserResult.Phonetic);
Assert.Contains(browserResult.Meanings, m => !string.IsNullOrEmpty(m.Example));
Assert.Equal("웹 브라우저", browserResult.Translations["ko"]);
// Act 4: Custom vocabulary registration
lookup.RegisterDefinition(
word: "hypersync",
phonetic: "/ˈhaɪ.pər.sɪŋk/",
meanings: new[]
{
new WordMeaning("noun", "A real-time state synchronization protocol for distributed browsers.", "PACA uses hypersync for tab handoff.")
},
translations: new Dictionary<string, string>
{
["ko"] = "고속 동기화",
["ja"] = "高速同期"
});
var hypersyncResult = lookup.Lookup("hypersync", targetLanguage: "ja");
Assert.True(hypersyncResult.Found);
Assert.Equal("高速同期", hypersyncResult.Translations["ja"]);
// Act 5: Unknown word graceful fallback
var unknownResult = lookup.Lookup("xyznonexistentword999");
Assert.False(unknownResult.Found);
Assert.Empty(unknownResult.Meanings);
Assert.Empty(unknownResult.Translations);
// Act 6: Async lookup with cancellation token
var asyncResult = await lookup.LookupAsync("cache", "ko");
Assert.True(asyncResult.Found);
Assert.Contains("캐시", asyncResult.Translations["ko"]);
}
[Fact]
public void Gate180_CaretBrowsingCoordinator_HandlesF7ToggleAndCursorNavigation()
{
// Arrange
var coordinator = new CaretBrowsingCoordinator();
string tabId = "tab-window-77";
// Assert initial state: Caret browsing disabled
Assert.False(coordinator.IsEnabled(tabId));
// Act 1: Toggle Caret Browsing with F7 key event
var eventFired = false;
CaretState? observedState = null;
coordinator.StateChanged += (sender, e) =>
{
eventFired = true;
observedState = e.State;
};
var keyResult1 = coordinator.HandleKeyDown(tabId, "F7");
// Assert 1: Toggled ON
Assert.True(keyResult1.Handled);
Assert.True(coordinator.IsEnabled(tabId));
Assert.True(eventFired);
Assert.NotNull(observedState);
Assert.True(observedState.IsEnabled);
// Act 2: Move cursor with arrow keys
coordinator.SetPosition(tabId, line: 10, column: 5, offset: 120);
var initialPos = coordinator.GetPosition(tabId);
Assert.Equal(10, initialPos.Line);
Assert.Equal(5, initialPos.Column);
Assert.Equal(120, initialPos.AbsoluteOffset);
// Right arrow moves column forward
var rightResult = coordinator.HandleKeyDown(tabId, "ArrowRight");
Assert.True(rightResult.Handled);
var posAfterRight = coordinator.GetPosition(tabId);
Assert.Equal(6, posAfterRight.Column);
Assert.Equal(121, posAfterRight.AbsoluteOffset);
// Left arrow moves column backward
coordinator.HandleKeyDown(tabId, "ArrowLeft");
var posAfterLeft = coordinator.GetPosition(tabId);
Assert.Equal(5, posAfterLeft.Column);
// Down arrow moves line down
coordinator.HandleKeyDown(tabId, "ArrowDown");
var posAfterDown = coordinator.GetPosition(tabId);
Assert.Equal(11, posAfterDown.Line);
// Up arrow moves line up
coordinator.HandleKeyDown(tabId, "ArrowUp");
var posAfterUp = coordinator.GetPosition(tabId);
Assert.Equal(10, posAfterUp.Line);
// Act 3: Shift + Arrow navigation expands selection
coordinator.SetPosition(tabId, line: 1, column: 0, offset: 0);
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: true);
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: true);
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: true);
var selection = coordinator.GetSelection(tabId);
Assert.Equal(0, selection.StartOffset);
Assert.Equal(3, selection.EndOffset);
Assert.Equal(3, selection.Length);
// Regular navigation without shift clears selection
coordinator.HandleKeyDown(tabId, "ArrowRight", shiftKey: false);
var clearedSelection = coordinator.GetSelection(tabId);
Assert.Equal(0, clearedSelection.Length);
// Act 4: Generate WebView2 injection script for caret overlay
var enableScript = coordinator.GenerateInjectionScript(enabled: true);
Assert.Contains("paca-caret-cursor", enableScript);
Assert.Contains("addEventListener('keydown'", enableScript);
var disableScript = coordinator.GenerateInjectionScript(enabled: false);
Assert.Contains("remove()", disableScript);
// Act 5: Toggle Caret Browsing OFF
var keyResult2 = coordinator.HandleKeyDown(tabId, "F7");
Assert.True(keyResult2.Handled);
Assert.False(coordinator.IsEnabled(tabId));
}
[Fact]
public void Gate181_HttpResponseHeadersViewer_ParsesAndAnalyzesSecurityAndCacheHeaders()
{
// Arrange
var viewer = new HttpResponseHeadersViewer();
// Act 1: Analyze secure response headers (HSTS, CSP, X-Frame-Options, X-Content-Type, Cache-Control)
var secureHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload",
["Content-Security-Policy"] = "default-src 'self'; script-src 'self' https://trusted.cdn.com; frame-ancestors 'none'; upgrade-insecure-requests",
["X-Frame-Options"] = "DENY",
["X-Content-Type-Options"] = "nosniff",
["Referrer-Policy"] = "strict-origin-when-cross-origin",
["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()",
["Cache-Control"] = "public, max-age=86400, must-revalidate",
["Content-Type"] = "text/html; charset=utf-8",
["Server"] = "cloudflare"
};
var analysis = viewer.Analyze(200, "OK", secureHeaders, protocol: "HTTP/2");
// Assert 1: Status & Summary
Assert.Equal(200, analysis.StatusCode);
Assert.Equal("OK", analysis.StatusDescription);
Assert.Equal("HTTP/2", analysis.Protocol);
Assert.NotEmpty(analysis.FormattedHeaders);
// Assert 1: Cache analysis
Assert.True(analysis.CacheAnalysis.IsCacheable);
Assert.True(analysis.CacheAnalysis.IsPublic);
Assert.False(analysis.CacheAnalysis.IsNoStore);
Assert.True(analysis.CacheAnalysis.IsMustRevalidate);
Assert.Equal(86400, analysis.CacheAnalysis.MaxAgeSeconds);
// Assert 1: Security audit & scoring
var sec = analysis.SecurityAudit;
Assert.True(sec.Hsts.IsPresent);
Assert.Equal(31536000, sec.Hsts.MaxAgeSeconds);
Assert.True(sec.Hsts.IncludeSubDomains);
Assert.True(sec.Hsts.Preload);
Assert.True(sec.Hsts.MeetsStrictCriteria);
Assert.True(sec.Csp.IsPresent);
Assert.True(sec.Csp.RestrictsScriptSrc);
Assert.True(sec.Csp.RestrictsFrameAncestors);
Assert.True(sec.Csp.UpgradesInsecureRequests);
Assert.Equal("DENY", sec.XFrameOptions);
Assert.Equal("nosniff", sec.XContentTypeOptions);
Assert.Equal("strict-origin-when-cross-origin", sec.ReferrerPolicy);
Assert.True(sec.SecurityScore >= 90);
Assert.Equal("A+", sec.Grade);
Assert.NotEmpty(sec.Strengths);
Assert.Empty(sec.Recommendations);
// Act 2: Analyze insecure / missing headers response
var insecureHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Cache-Control"] = "no-store, no-cache",
["Content-Type"] = "text/html"
};
var insecureAnalysis = viewer.Analyze(200, "OK", insecureHeaders, protocol: "HTTP/1.1");
// Assert 2: Insecure analysis
Assert.False(insecureAnalysis.CacheAnalysis.IsCacheable);
Assert.True(insecureAnalysis.CacheAnalysis.IsNoStore);
Assert.True(insecureAnalysis.CacheAnalysis.IsNoCache);
var insecureSec = insecureAnalysis.SecurityAudit;
Assert.False(insecureSec.Hsts.IsPresent);
Assert.False(insecureSec.Csp.IsPresent);
Assert.Null(insecureSec.XFrameOptions);
Assert.True(insecureSec.SecurityScore < 35);
Assert.Equal("F", insecureSec.Grade);
Assert.Contains(insecureSec.Recommendations, r => r.Contains("HSTS", StringComparison.OrdinalIgnoreCase));
Assert.Contains(insecureSec.Recommendations, r => r.Contains("CSP", StringComparison.OrdinalIgnoreCase));
// Act 3: Formatted raw text summary output
var rawText = viewer.FormatAsRawText(200, "OK", secureHeaders, protocol: "HTTP/2");
Assert.StartsWith("HTTP/2 200 OK", rawText);
Assert.Contains("Strict-Transport-Security:", rawText);
Assert.Contains("Cache-Control:", rawText);
}
}