|
1 | 1 | using System.Net; |
| 2 | +using HtmlAgilityPack; |
| 3 | +using EssentialCSharp.Web.Services; |
| 4 | +using Microsoft.AspNetCore.Hosting; |
| 5 | +using Microsoft.AspNetCore.Mvc.Testing; |
| 6 | +using Microsoft.Extensions.DependencyInjection; |
2 | 7 |
|
3 | 8 | namespace EssentialCSharp.Web.Tests; |
4 | 9 |
|
@@ -42,6 +47,72 @@ public async Task WhenPagesAreAccessed_TheyReturnHtml(string relativeUrl) |
42 | 47 | await Assert.That(content).Contains("<html", StringComparison.OrdinalIgnoreCase); |
43 | 48 | } |
44 | 49 |
|
| 50 | + [Test] |
| 51 | + [Arguments("/about?rid=abc", "https://essentialcsharp.com/about", "Learn about Essential C#")] |
| 52 | + [Arguments("/home", "https://essentialcsharp.com/home", "Explore Essential C#")] |
| 53 | + [Arguments("/cookie-policy", "https://essentialcsharp.com/cookie-policy", "uses cookies")] |
| 54 | + [Arguments("/guidelines", "https://essentialcsharp.com/guidelines", "coding guidelines")] |
| 55 | + [Arguments("/announcements", "https://essentialcsharp.com/announcements", "announcements")] |
| 56 | + [Arguments("/TermsOfService", "https://essentialcsharp.com/TermsOfService", "terms of service")] |
| 57 | + [Arguments("/mcp-setup", "https://essentialcsharp.com/mcp-setup", "Model Context Protocol")] |
| 58 | + [Arguments("/introducing-c", "https://essentialcsharp.com/introducing-c", "Introducing C#")] |
| 59 | + [Arguments("/hello-world", "https://essentialcsharp.com/introducing-c", "Hello")] |
| 60 | + public async Task PagesHaveOneCanonicalAndPageSpecificDescriptions(string path, string expectedCanonical, string expectedDescription) |
| 61 | + { |
| 62 | + await using WebApplicationFactory<Program> factory = CreateFactoryWithBaseUrl("https://essentialcsharp.com"); |
| 63 | + await AssertPageMetadata(factory, path, expectedCanonical, expectedDescription); |
| 64 | + } |
| 65 | + |
| 66 | + [Test] |
| 67 | + [Arguments("/about", "https://dev.essentialcsharp.com/about", "Learn about Essential C#")] |
| 68 | + [Arguments("/introducing-c", "https://dev.essentialcsharp.com/introducing-c", "Introducing C#")] |
| 69 | + [Arguments("/hello-world?rid=abc", "https://dev.essentialcsharp.com/introducing-c", "Hello")] |
| 70 | + public async Task CanonicalUrlsUseConfiguredDeploymentOrigin(string path, string expectedCanonical, string expectedDescription) |
| 71 | + { |
| 72 | + await using WebApplicationFactory<Program> factory = CreateFactoryWithBaseUrl("https://dev.essentialcsharp.com/"); |
| 73 | + await AssertPageMetadata(factory, path, expectedCanonical, expectedDescription); |
| 74 | + } |
| 75 | + |
| 76 | + private WebApplicationFactory<Program> CreateFactoryWithBaseUrl(string baseUrl) => |
| 77 | + Factory.Inner.WithWebHostBuilder(builder => builder.ConfigureServices(services => |
| 78 | + services.PostConfigure<SiteSettings>(settings => settings.BaseUrl = baseUrl))); |
| 79 | + |
| 80 | + private static async Task AssertPageMetadata(WebApplicationFactory<Program> factory, string path, string expectedCanonical, string expectedDescription) |
| 81 | + { |
| 82 | + using HttpClient client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); |
| 83 | + using HttpResponseMessage response = await GetFollowingGetRedirectsAsync(client, path); |
| 84 | + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); |
| 85 | + |
| 86 | + HtmlDocument document = new(); |
| 87 | + document.LoadHtml(await response.Content.ReadAsStringAsync()); |
| 88 | + HtmlNode head = document.DocumentNode.SelectSingleNode("//head") ?? throw new InvalidOperationException("Missing head element"); |
| 89 | + HtmlNodeCollection canonicals = head.SelectNodes("link[@rel='canonical']") ?? throw new InvalidOperationException("Missing canonical link"); |
| 90 | + await Assert.That(canonicals.Count).IsEqualTo(1); |
| 91 | + await Assert.That(canonicals[0].GetAttributeValue("href", "")).IsEqualTo(expectedCanonical); |
| 92 | + await Assert.That((head.SelectSingleNode("meta[@property='og:url']") ?? throw new InvalidOperationException("Missing OG URL")).GetAttributeValue("content", "")).IsEqualTo(expectedCanonical); |
| 93 | + |
| 94 | + string description = (head.SelectSingleNode("meta[@name='description']") ?? throw new InvalidOperationException("Missing description")).GetAttributeValue("content", ""); |
| 95 | + await Assert.That(description).Contains(expectedDescription); |
| 96 | + await Assert.That((head.SelectNodes("meta[@name='description']") ?? throw new InvalidOperationException("Missing description")).Count).IsEqualTo(1); |
| 97 | + await Assert.That((head.SelectSingleNode("meta[@property='og:description']") ?? throw new InvalidOperationException("Missing OG description")).GetAttributeValue("content", "")).IsEqualTo(description); |
| 98 | + await Assert.That((head.SelectSingleNode("meta[@name='twitter:description']") ?? throw new InvalidOperationException("Missing Twitter description")).GetAttributeValue("content", "")).IsEqualTo(description); |
| 99 | + } |
| 100 | + |
| 101 | + [Test] |
| 102 | + public async Task NotFoundPageKeepsDescriptionFallbackWithoutCanonical() |
| 103 | + { |
| 104 | + using HttpClient client = CreateClientWithoutAutoRedirect(); |
| 105 | + using HttpResponseMessage response = await GetFollowingGetRedirectsAsync(client, "/non-existing-page1234"); |
| 106 | + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound); |
| 107 | + |
| 108 | + HtmlDocument document = new(); |
| 109 | + document.LoadHtml(await response.Content.ReadAsStringAsync()); |
| 110 | + HtmlNode head = document.DocumentNode.SelectSingleNode("//head") ?? throw new InvalidOperationException("Missing head element"); |
| 111 | + await Assert.That(head.SelectNodes("link[@rel='canonical']")).IsNull(); |
| 112 | + await Assert.That((head.SelectSingleNode("meta[@name='description']") ?? throw new InvalidOperationException("Missing description")).GetAttributeValue("content", "")) |
| 113 | + .Contains("Accelerate your development knowledge"); |
| 114 | + } |
| 115 | + |
45 | 116 | [Test] |
46 | 117 | public async Task WhenTheApplicationStarts_NonExistingPage_GivesCorrectStatusCode() |
47 | 118 | { |
|
0 commit comments