diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 694ca8c5b..d9dfb8358 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -363,6 +363,18 @@ private YamlFrontMatter ProcessYamlFrontMatter(MarkdownDocument document) Collector.Emit(severity, FilePath, message); } + // Validate mapped_pages URLs + if (fm.MappedPages is not null) + { + foreach (var url in fm.MappedPages) + { + if (!string.IsNullOrEmpty(url) && (!url.StartsWith("https://www.elastic.co/guide", StringComparison.OrdinalIgnoreCase) || !Uri.IsWellFormedUriString(url, UriKind.Absolute))) + { + Collector.EmitError(FilePath, $"Invalid mapped_pages URL: \"{url}\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide."); + } + } + } + // TODO remove when migration tool and our demo content sets are updated var deprecatedTitle = fm.Title; if (!string.IsNullOrEmpty(deprecatedTitle)) diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs index 7c87e29ce..87548cf92 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs @@ -183,3 +183,156 @@ public void HasErrors() Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid products frontmatter value: \"Product 'id' field is required.")); } } + +public class MappedPagesValidUrl(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html" + --- + + # Test Page + """ +) +{ + [Fact] + public void NoErrors() + { + Collector.Diagnostics.Should().BeEmpty(); + } +} + +public class MappedPagesInvalidUrl(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "https://www.elastic.co/docs/get-started/deployment-options" + --- + + # Test Page + """ +) +{ + [Fact] + public void HasErrors() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"https://www.elastic.co/docs/get-started/deployment-options\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide.")); + } +} + +public class MappedPagesMixedUrls(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html" + - "https://www.elastic.co/docs/invalid-url" + - "https://www.elastic.co/guide/en/kibana/current/index.html" + --- + + # Test Page + """ +) +{ + [Fact] + public void HasErrorsForInvalidUrl() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"https://www.elastic.co/docs/invalid-url\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"")); + } +} + +public class MappedPagesEmptyUrl(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "" + --- + + # Test Page + """ +) +{ + [Fact] + public void NoErrorsForEmptyUrl() + { + // Empty URLs are ignored, no validation error should occur + Collector.Diagnostics.Should().BeEmpty(); + } +} + +public class MappedPagesExternalUrl(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "https://github.com/elastic/docs-builder" + --- + + # Test Page + """ +) +{ + [Fact] + public void HasErrorsForExternalUrl() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"https://github.com/elastic/docs-builder\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"")); + } +} + +public class MappedPagesMalformedUri(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "https://www.elastic.co/guide/[invalid-characters]" + --- + + # Test Page + """ +) +{ + [Fact] + public void HasErrorsForMalformedUri() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"https://www.elastic.co/guide/[invalid-characters]\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"")); + } +} + +public class MappedPagesInvalidScheme(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "https://www.elastic.co/guide/invalid uri with spaces" + --- + + # Test Page + """ +) +{ + [Fact] + public void HasErrorsForInvalidScheme() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"https://www.elastic.co/guide/invalid uri with spaces\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"")); + } +} + +public class MappedPagesNotAbsoluteUri(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + mapped_pages: + - "not-a-uri-at-all" + --- + + # Test Page + """ +) +{ + [Fact] + public void HasErrorsForNotAbsoluteUri() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"not-a-uri-at-all\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"")); + } +}