1+ // Licensed to Elasticsearch B.V under one or more agreements.
2+ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+ // See the LICENSE file in the project root for more information
4+
5+ namespace authoring
6+
7+ open System
8+ open System.Collections .Generic
9+ open System.Collections .Frozen
10+ open System.IO .Abstractions .TestingHelpers
11+ open Elastic.Documentation .Diagnostics
12+ open Elastic.Documentation .Links
13+ open Elastic.Markdown .Links .CrossLinks
14+ open Elastic.Documentation
15+ open Swensen.Unquote
16+ open Elastic.Documentation .Configuration .Builder
17+ open authoring
18+
19+ module CrossLinkResolverAssertions =
20+
21+ let private parseRedirectsYaml ( redirectsYamlContent : string ) ( collector : IDiagnosticsCollector ) =
22+ let mockFileSystem = MockFileSystem()
23+ let fullYaml = sprintf " redirects:\n %s " ( redirectsYamlContent.Replace( " \r\n " , " \n " ) .Replace( " \n " , " \n " ))
24+ let mockRedirectsFilePath = " mock_redirects.yml"
25+ mockFileSystem.AddFile( mockRedirectsFilePath, MockFileData( fullYaml))
26+ let mockRedirectsFile = mockFileSystem.FileInfo.New( mockRedirectsFilePath)
27+
28+ let docContext =
29+ { new IDocumentationContext with
30+ member _.Collector = collector
31+ member _.DocumentationSourceDirectory = mockFileSystem.DirectoryInfo.New( " /docs" )
32+ member _.Git = GitCheckoutInformation.Unavailable
33+ member _.ReadFileSystem = mockFileSystem
34+ member _.WriteFileSystem = mockFileSystem
35+ member _.ConfigurationPath = mockFileSystem.FileInfo.New( " mock_docset.yml" )
36+ }
37+ let redirectFileParser = RedirectFile( mockRedirectsFile, docContext)
38+ redirectFileParser.Redirects
39+
40+ // Helper to create FetchedCrossLinks
41+ let private createFetchedCrossLinks ( redirectsYamlSnippet : string ) ( linksData : IDictionary < string , LinkMetadata >) repoName =
42+ let collector = TestDiagnosticsCollector() :> IDiagnosticsCollector
43+ let redirectRules = parseRedirectsYaml redirectsYamlSnippet collector
44+
45+ if collector.Errors > 0 then
46+ failwithf $" Failed to parse redirects YAML: %A {collector}"
47+
48+ let linkReference =
49+ LinkReference(
50+ Origin = GitCheckoutInformation.Unavailable,
51+ UrlPathPrefix = null ,
52+ Links = Dictionary( linksData),
53+ CrossLinks = Array.empty< string>,
54+ Redirects = redirectRules
55+ )
56+
57+ let declaredRepos = HashSet< string>()
58+ declaredRepos.Add( repoName) |> ignore
59+
60+ FetchedCrossLinks(
61+ DeclaredRepositories = declaredRepos,
62+ LinkReferences = FrozenDictionary.ToFrozenDictionary( dict [ repoName, linkReference]),
63+ FromConfiguration = true ,
64+ LinkIndexEntries = FrozenDictionary< string, LinkRegistryEntry>. Empty
65+ )
66+
67+ let private redirectsYaml = """
68+ # test scenario 1
69+ 'testing/redirects/multi-topic-page-1-old.md':
70+ to: 'testing/redirects/multi-topic-page-1-new-anchorless.md'
71+ anchors: { "!": null }
72+ many:
73+ - to: 'testing/redirects/multi-topic-page-1-new-topic-a-subpage.md'
74+ anchors: {'topic-a-intro': null, 'topic-a-details': 'details-anchor'}
75+ - to: 'testing/redirects/multi-topic-page-1-new-topic-b-subpage.md'
76+ anchors: {'topic-b-main': 'main-anchor'}
77+ - to: 'testing/redirects/multi-topic-page-1-old.md'
78+ anchors: {'topic-c-main': 'topic-c-main'}
79+ # test scenario 2
80+ 'testing/redirects/multi-topic-page-2-old.md':
81+ to: 'testing/redirects/multi-topic-page-2-old.md'
82+ anchors: {} # This means pass through any anchor for the default 'to'
83+ many:
84+ - to: 'testing/redirects/multi-topic-page-2-new-topic-a-subpage.md'
85+ anchors: {'topic-a-intro': 'introduction', 'topic-a-details': null}
86+ - to: 'testing/redirects/multi-topic-page-2-new-topic-b-subpage.md'
87+ anchors: {'topic-b-main': 'summary', 'topic-b-config': null}
88+ """
89+
90+ let private mockLinksData =
91+ dict [
92+ ( " testing/redirects/multi-topic-page-1-new-anchorless.md" , LinkMetadata( Anchors = null , Hidden = false ))
93+ ( " testing/redirects/multi-topic-page-1-new-topic-a-subpage.md" , LinkMetadata( Anchors = [| " details-anchor" ; " introduction" |], Hidden = false ))
94+ ( " testing/redirects/multi-topic-page-1-new-topic-b-subpage.md" , LinkMetadata( Anchors = [| " main-anchor" |], Hidden = false ))
95+ ( " testing/redirects/multi-topic-page-1-old.md" , LinkMetadata( Anchors = [| " topic-c-main" ; " unmatched-anchor" |], Hidden = false ))
96+ ( " testing/redirects/multi-topic-page-2-old.md" , LinkMetadata( Anchors = [| " unmatched-anchor" ; " topic-c-main" ; " topic-a-intro" ; " topic-a-details" ; " topic-b-main" ; " topic-b-config" |], Hidden = false ))
97+ ( " testing/redirects/multi-topic-page-2-new-topic-a-subpage.md" , LinkMetadata( Anchors = [| " introduction" ; " summary" |], Hidden = false ))
98+ ( " testing/redirects/multi-topic-page-2-new-topic-b-subpage.md" , LinkMetadata( Anchors = [| " summary" |], Hidden = false ))
99+ ] :> IDictionary<_,_>
100+
101+
102+ let private repoName = " docs-content"
103+ let private fetchedLinks = createFetchedCrossLinks redirectsYaml mockLinksData repoName
104+ let private uriResolver = IsolatedBuildEnvironmentUriResolver() :> IUriEnvironmentResolver
105+ let private baseExpectedUrl = $" https://docs-v3-preview.elastic.dev/elastic/{repoName}/tree/main"
106+
107+ let resolvesTo ( inputUrl : string ) ( expectedPathWithOptionalAnchor : string ) =
108+ let mutable errors = List.empty
109+ let errorEmitter ( msg : string ) = errors <- msg :: errors
110+ let inputUri = Uri( inputUrl)
111+ let mutable resolvedUri : Uri = Unchecked.defaultof< Uri>
112+
113+ let success = CrossLinkResolver.TryResolve( Action<_>( errorEmitter), fetchedLinks, uriResolver, inputUri, & resolvedUri)
114+
115+ if not errors.IsEmpty then
116+ failwithf $" Resolution for '%s {inputUrl}' failed with errors: %A {errors}"
117+
118+ test <@ success @>
119+ match box resolvedUri with
120+ | null -> failwithf $" Resolved URI was null for input '%s {inputUrl}' even though TryResolve returned true."
121+ | _ ->
122+ let expectedFullUrl = baseExpectedUrl + expectedPathWithOptionalAnchor
123+ test <@ resolvedUri.ToString() = expectedFullUrl @>
0 commit comments