11using System . Reflection ;
2+ using ShellDocs . Core ;
23using Xunit ;
34
45namespace ShellDocs . Tests ;
@@ -8,6 +9,9 @@ public class BuildCommandTests : IDisposable
89 private readonly string _tempDir ;
910 private readonly MethodInfo _rewrite ;
1011 private readonly MethodInfo _copy ;
12+ private readonly MethodInfo _writeSitemap ;
13+ private readonly MethodInfo _writeRobots ;
14+ private readonly MethodInfo _injectOg ;
1115
1216 public BuildCommandTests ( )
1317 {
@@ -18,8 +22,11 @@ public BuildCommandTests()
1822 . FirstOrDefault ( a => a . GetName ( ) . Name == "shelldocs" )
1923 ?? Assembly . Load ( "shelldocs" ) ;
2024 var type = cli . GetType ( "ShellDocs.CLI.Commands.BuildCommand" , throwOnError : true ) ! ;
21- _rewrite = type . GetMethod ( "RewriteBaseHrefInAllHtml" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
22- _copy = type . GetMethod ( "CopyDirectoryMerging" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
25+ _rewrite = type . GetMethod ( "RewriteBaseHrefInAllHtml" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
26+ _copy = type . GetMethod ( "CopyDirectoryMerging" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
27+ _writeSitemap = type . GetMethod ( "WriteSitemap" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
28+ _writeRobots = type . GetMethod ( "WriteRobots" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
29+ _injectOg = type . GetMethod ( "InjectOgMeta" , BindingFlags . Static | BindingFlags . NonPublic | BindingFlags . Public ) ! ;
2330 }
2431
2532 public void Dispose ( )
@@ -33,6 +40,26 @@ private int RewriteBaseHrefInAllHtml(string outputDir, string href) =>
3340 private void CopyDirectoryMerging ( string source , string dest ) =>
3441 _copy . Invoke ( null , new object [ ] { source , dest } ) ;
3542
43+ private void WriteSitemap ( string outputDir , string siteUrl , IReadOnlyList < string > urls ) =>
44+ _writeSitemap . Invoke ( null , new object [ ] { outputDir , siteUrl , urls } ) ;
45+
46+ private void WriteRobots ( string outputDir , string siteUrl ) =>
47+ _writeRobots . Invoke ( null , new object [ ] { outputDir , siteUrl } ) ;
48+
49+ private int InjectOgMeta ( string outputDir , string siteUrl , NavigationGraph graph ) =>
50+ ( int ) _injectOg . Invoke ( null , new object [ ] { outputDir , siteUrl , graph } ) ! ;
51+
52+ // NavigationNode.Children/Parent have `internal set` — bypass via reflection
53+ // so tests can build a graph without exposing the setters or standing up a
54+ // temp content directory.
55+ private static readonly PropertyInfo _childrenProp = typeof ( NavigationNode ) . GetProperty ( "Children" ) ! ;
56+ private static readonly PropertyInfo _parentProp = typeof ( NavigationNode ) . GetProperty ( "Parent" ) ! ;
57+ private static void LinkChildren ( NavigationNode parent , params NavigationNode [ ] children )
58+ {
59+ _childrenProp . SetValue ( parent , children ) ;
60+ foreach ( var c in children ) _parentProp . SetValue ( c , parent ) ;
61+ }
62+
3663 [ Theory ]
3764 [ InlineData ( "<base href=\" /\" />" , "/repo/" , "<base href=\" /repo/\" />" ) ]
3865 [ InlineData ( "<base href='/'/>" , "/repo/" , "<base href=\" /repo/\" />" ) ]
@@ -129,4 +156,114 @@ public void CopyDirectoryMerging_CopiesMissingFilesEvenWhenSomeExist()
129156 Assert . Equal ( "dst-a" , File . ReadAllText ( Path . Combine ( dst , "a.txt" ) ) ) ;
130157 Assert . Equal ( "src-b" , File . ReadAllText ( Path . Combine ( dst , "b.txt" ) ) ) ;
131158 }
159+
160+ [ Fact ]
161+ public void WriteSitemap_ProducesValidUrlSet ( )
162+ {
163+ WriteSitemap ( _tempDir , "https://example.com" , new [ ] { "/" , "/docs/introduction" , "/docs/cli/build" } ) ;
164+
165+ var xml = File . ReadAllText ( Path . Combine ( _tempDir , "sitemap.xml" ) ) ;
166+ Assert . Contains ( "<?xml version=\" 1.0\" encoding=\" UTF-8\" ?>" , xml ) ;
167+ Assert . Contains ( "<urlset xmlns=\" http://www.sitemaps.org/schemas/sitemap/0.9\" >" , xml ) ;
168+ Assert . Contains ( "<loc>https://example.com/</loc>" , xml ) ;
169+ Assert . Contains ( "<loc>https://example.com/docs/introduction</loc>" , xml ) ;
170+ Assert . Contains ( "<loc>https://example.com/docs/cli/build</loc>" , xml ) ;
171+ Assert . Contains ( "</urlset>" , xml ) ;
172+ }
173+
174+ [ Fact ]
175+ public void WriteSitemap_DeduplicatesUrls ( )
176+ {
177+ WriteSitemap ( _tempDir , "https://example.com" , new [ ] { "/" , "/" , "/docs/x" , "/docs/x" } ) ;
178+
179+ var xml = File . ReadAllText ( Path . Combine ( _tempDir , "sitemap.xml" ) ) ;
180+ Assert . Equal ( 2 , System . Text . RegularExpressions . Regex . Matches ( xml , "<url>" ) . Count ) ;
181+ }
182+
183+ [ Fact ]
184+ public void WriteRobots_IncludesSitemapReference ( )
185+ {
186+ WriteRobots ( _tempDir , "https://example.com" ) ;
187+ var txt = File . ReadAllText ( Path . Combine ( _tempDir , "robots.txt" ) ) ;
188+
189+ Assert . Contains ( "User-agent: *" , txt ) ;
190+ Assert . Contains ( "Allow: /" , txt ) ;
191+ Assert . Contains ( "Sitemap: https://example.com/sitemap.xml" , txt ) ;
192+ }
193+
194+ [ Fact ]
195+ public void InjectOgMeta_AddsOgTagsBeforeHeadClose ( )
196+ {
197+ var pagePath = Path . Combine ( _tempDir , "docs" , "intro" , "index.html" ) ;
198+ Directory . CreateDirectory ( Path . GetDirectoryName ( pagePath ) ! ) ;
199+ File . WriteAllText ( pagePath , "<html><head><title>t</title></head><body>b</body></html>" ) ;
200+
201+ var pageNode = new NavigationNode
202+ {
203+ Url = "/docs/intro" ,
204+ Title = "Introduction" ,
205+ Description = "What ShellDocs is." ,
206+ Kind = NodeKind . Page ,
207+ } ;
208+ var root = new NavigationNode { Url = "/" , Kind = NodeKind . Section } ;
209+ LinkChildren ( root , pageNode ) ;
210+ var graph = new NavigationGraph ( root ) ;
211+
212+ var count = InjectOgMeta ( _tempDir , "https://example.com" , graph ) ;
213+
214+ Assert . Equal ( 1 , count ) ;
215+ var html = File . ReadAllText ( pagePath ) ;
216+ Assert . Contains ( "<meta property=\" og:type\" content=\" article\" />" , html ) ;
217+ Assert . Contains ( "<meta property=\" og:url\" content=\" https://example.com/docs/intro\" />" , html ) ;
218+ Assert . Contains ( "<meta property=\" og:title\" content=\" Introduction\" />" , html ) ;
219+ Assert . Contains ( "<meta property=\" og:description\" content=\" What ShellDocs is.\" />" , html ) ;
220+ // Injected before </head>, not after.
221+ var ogIdx = html . IndexOf ( "og:type" , StringComparison . Ordinal ) ;
222+ var closeIdx = html . IndexOf ( "</head>" , StringComparison . Ordinal ) ;
223+ Assert . True ( ogIdx > 0 && ogIdx < closeIdx , "og:type meta must appear before </head>" ) ;
224+ }
225+
226+ [ Fact ]
227+ public void InjectOgMeta_SkipsMissingHtmlFilesGracefully ( )
228+ {
229+ var pageNode = new NavigationNode
230+ {
231+ Url = "/nowhere" ,
232+ Title = "Ghost" ,
233+ Description = "Not on disk." ,
234+ Kind = NodeKind . Page ,
235+ } ;
236+ var root = new NavigationNode { Url = "/" , Kind = NodeKind . Section } ;
237+ LinkChildren ( root , pageNode ) ;
238+ var graph = new NavigationGraph ( root ) ;
239+
240+ var count = InjectOgMeta ( _tempDir , "https://example.com" , graph ) ;
241+
242+ Assert . Equal ( 0 , count ) ;
243+ }
244+
245+ [ Fact ]
246+ public void InjectOgMeta_EncodesSpecialCharacters ( )
247+ {
248+ var pagePath = Path . Combine ( _tempDir , "p" , "index.html" ) ;
249+ Directory . CreateDirectory ( Path . GetDirectoryName ( pagePath ) ! ) ;
250+ File . WriteAllText ( pagePath , "<html><head></head></html>" ) ;
251+
252+ var pageNode = new NavigationNode
253+ {
254+ Url = "/p" ,
255+ Title = "AT&T <spec>" ,
256+ Description = "Uses & and <" ,
257+ Kind = NodeKind . Page ,
258+ } ;
259+ var root = new NavigationNode { Url = "/" , Kind = NodeKind . Section } ;
260+ LinkChildren ( root , pageNode ) ;
261+ var graph = new NavigationGraph ( root ) ;
262+
263+ InjectOgMeta ( _tempDir , "https://example.com" , graph ) ;
264+
265+ var html = File . ReadAllText ( pagePath ) ;
266+ Assert . Contains ( "AT&T <spec>" , html ) ;
267+ Assert . Contains ( "Uses & and <" , html ) ;
268+ }
132269}
0 commit comments