1+ using System ;
2+ using System . IO ;
3+ using System . Text ;
4+ using System . Threading . Tasks ;
5+ using Tanka . DocsTool . Markdown ;
6+ using Xunit ;
7+
8+ namespace Tanka . DocsTool . Tests . Markdown
9+ {
10+ public class DocsMarkdownServiceFacts
11+ {
12+ [ Fact ]
13+ public async Task RenderPage_WithMemoryStream_ShouldNotThrowClosedStreamException ( )
14+ {
15+ // Given - Create a DocsMarkdownService
16+ var service = new DocsMarkdownService ( new Markdig . MarkdownPipelineBuilder ( ) ) ;
17+
18+ // Create markdown content with emojis (reproducing the original issue)
19+ var markdownContent = @"---
20+ title: Test Page
21+ ---
22+
23+ # Test Page 🚀
24+
25+ This is a test page with emoji content that caused the original issue.
26+
27+ Content includes:
28+ - Emoji characters: 🎉 ✨ 📝
29+ - Regular text
30+ - More content to make it substantial
31+ " ;
32+
33+ // Create input stream (simulating processedStream in PageComposer)
34+ await using var inputStream = new MemoryStream ( Encoding . UTF8 . GetBytes ( markdownContent ) ) ;
35+ await using var outputStream = new MemoryStream ( ) ;
36+
37+ // When - Call RenderPage (this should reproduce the "Cannot access a closed Stream" error)
38+ var exception = await Record . ExceptionAsync ( async ( ) =>
39+ {
40+ await service . RenderPage ( inputStream , outputStream ) ;
41+ } ) ;
42+
43+ // Then - Should not throw any exception
44+ Assert . Null ( exception ) ;
45+
46+ // Verify output was generated
47+ Assert . True ( outputStream . Length > 0 ) ;
48+
49+ // Verify we can still read from the input stream after RenderPage
50+ inputStream . Position = 0 ;
51+ using var reader = new StreamReader ( inputStream , Encoding . UTF8 ) ;
52+ var content = await reader . ReadToEndAsync ( ) ;
53+ Assert . Contains ( "Test Page" , content ) ;
54+ }
55+
56+ [ Fact ]
57+ public async Task RenderPage_SimulatePageComposerScenario_ShouldHandleStreamLifetime ( )
58+ {
59+ // Given - Simulate the exact scenario from PageComposer.ComposePartialHtmlPage
60+ var service = new DocsMarkdownService ( new Markdig . MarkdownPipelineBuilder ( ) ) ;
61+
62+ var markdownContent = @"---
63+ title: Architecture Overview
64+ ---
65+
66+ # Architecture Overview 🏗️
67+
68+ System design includes:
69+ - Pipeline architecture
70+ - Multi-layer file system
71+ - Content processing chain
72+ " ;
73+
74+ Exception ? caughtException = null ;
75+ string contentPreview = "" ;
76+
77+ // When - Simulate the exact flow from PageComposer
78+ await using var processedStream = new MemoryStream ( Encoding . UTF8 . GetBytes ( markdownContent ) ) ;
79+ processedStream . Position = 0 ;
80+
81+ await using var outputStream = new MemoryStream ( ) ;
82+
83+ try
84+ {
85+ var frontmatter = await service . RenderPage ( processedStream , outputStream ) ;
86+ // This should succeed without throwing
87+ Assert . NotNull ( frontmatter ) ;
88+ Assert . Equal ( "Architecture Overview" , frontmatter . Title ) ;
89+ }
90+ catch ( Exception e )
91+ {
92+ caughtException = e ;
93+
94+ // Simulate the error handling code from PageComposer that tries to read the stream
95+ try
96+ {
97+ processedStream . Position = 0 ;
98+ using var debugReader = new StreamReader ( processedStream , Encoding . UTF8 , leaveOpen : true ) ;
99+ var buffer = new char [ 100 ] ;
100+ var charsRead = await debugReader . ReadAsync ( buffer , 0 , buffer . Length ) ;
101+ contentPreview = new string ( buffer , 0 , charsRead ) ;
102+ processedStream . Position = 0 ;
103+ }
104+ catch ( Exception debugEx )
105+ {
106+ // This is where we might see "Cannot access a closed Stream"
107+ contentPreview = $ "[Debug failed: { debugEx . Message } ]";
108+ }
109+ }
110+
111+ // Then - Should not have any exception
112+ if ( caughtException != null )
113+ {
114+ Assert . True ( false , $ "Unexpected exception: { caughtException . Message } . Content preview: { contentPreview } ") ;
115+ }
116+ }
117+ }
118+ }
0 commit comments