1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Threading . Tasks ;
4+ using AutoFixture ;
5+ using Contentstack . Core . Configuration ;
6+ using Contentstack . Core . Unit . Tests . Helpers ;
7+ using Contentstack . Core . Unit . Tests . Mokes ;
8+ using Xunit ;
9+
10+ namespace Contentstack . Core . Unit . Tests
11+ {
12+ /// <summary>
13+ /// Unit tests for ContentstackClient.Fork() method
14+ /// Tests client forking behavior, isolation, and configuration preservation
15+ /// </summary>
16+ [ Trait ( "Category" , "TimelinePreview" ) ]
17+ [ Trait ( "Category" , "Fork" ) ]
18+ public class ContentstackClientForkTests : ContentstackClientTestBase
19+ {
20+ #region Positive Test Cases
21+
22+ [ Fact ]
23+ public void Fork_CreatesIndependentClientInstance ( )
24+ {
25+ // Arrange
26+ var parentClient = CreateClientWithTimeline ( ) ;
27+
28+ // Act
29+ var forkedClient = parentClient . Fork ( ) ;
30+
31+ // Assert
32+ Assert . NotNull ( forkedClient ) ;
33+ AssertClientsAreIndependent ( parentClient , forkedClient ) ;
34+ }
35+
36+ [ Fact ]
37+ public void Fork_PreservesBaseConfiguration ( )
38+ {
39+ // Arrange
40+ var parentClient = CreateClientWithTimeline ( ) ;
41+
42+ // Act
43+ var forkedClient = parentClient . Fork ( ) ;
44+
45+ // Assert
46+ AssertConfigurationPreserved ( parentClient , forkedClient ) ;
47+ }
48+
49+ [ Fact ]
50+ public void Fork_PreservesCustomHeaders ( )
51+ {
52+ // Arrange
53+ var parentClient = CreateClientWithTimeline ( ) ;
54+ var customHeaderKey = "X-Custom-Header" ;
55+ var customHeaderValue = _fixture . Create < string > ( ) ;
56+
57+ parentClient . SetHeader ( customHeaderKey , customHeaderValue ) ;
58+
59+ // Act
60+ var forkedClient = parentClient . Fork ( ) ;
61+
62+ // Assert
63+ // Verify custom header is preserved in forked client
64+ var parentHeaders = GetInternalField < Dictionary < string , object > > ( parentClient , "_LocalHeaders" ) ;
65+ var forkedHeaders = GetInternalField < Dictionary < string , object > > ( forkedClient , "_LocalHeaders" ) ;
66+
67+ Assert . True ( parentHeaders . ContainsKey ( customHeaderKey ) ) ;
68+ Assert . True ( forkedHeaders . ContainsKey ( customHeaderKey ) ) ;
69+ Assert . Equal ( parentHeaders [ customHeaderKey ] , forkedHeaders [ customHeaderKey ] ) ;
70+ }
71+
72+ [ Fact ]
73+ public void Fork_CopiesContentTypeUidHints ( )
74+ {
75+ // Arrange
76+ var parentClient = CreateClientWithTimeline ( ) ;
77+ var contentTypeUid = _fixture . Create < string > ( ) ;
78+ var entryUid = _fixture . Create < string > ( ) ;
79+
80+ // Set content type and entry hints on parent
81+ SetInternalField ( parentClient , "currentContenttypeUid" , contentTypeUid ) ;
82+ SetInternalField ( parentClient , "currentEntryUid" , entryUid ) ;
83+
84+ // Act
85+ var forkedClient = parentClient . Fork ( ) ;
86+
87+ // Assert
88+ var forkedContentTypeUid = GetInternalField < string > ( forkedClient , "currentContenttypeUid" ) ;
89+ var forkedEntryUid = GetInternalField < string > ( forkedClient , "currentEntryUid" ) ;
90+
91+ Assert . Equal ( contentTypeUid , forkedContentTypeUid ) ;
92+ Assert . Equal ( entryUid , forkedEntryUid ) ;
93+ }
94+
95+ [ Fact ]
96+ public void Fork_IndependentLivePreviewConfig ( )
97+ {
98+ // Arrange
99+ var parentClient = CreateClientWithTimeline ( ) ;
100+ var parentConfig = parentClient . GetLivePreviewConfig ( ) ;
101+ parentConfig . PreviewTimestamp = "2024-11-29T14:30:00.000Z" ;
102+
103+ // Act
104+ var forkedClient = parentClient . Fork ( ) ;
105+ var forkedConfig = forkedClient . GetLivePreviewConfig ( ) ;
106+
107+ // Assert - Configs are independent instances
108+ Assert . NotSame ( parentConfig , forkedConfig ) ;
109+
110+ // Assert - Initial values are copied
111+ Assert . Equal ( parentConfig . PreviewTimestamp , forkedConfig . PreviewTimestamp ) ;
112+ Assert . Equal ( parentConfig . ReleaseId , forkedConfig . ReleaseId ) ;
113+ Assert . Equal ( parentConfig . Enable , forkedConfig . Enable ) ;
114+ Assert . Equal ( parentConfig . Host , forkedConfig . Host ) ;
115+ Assert . Equal ( parentConfig . ManagementToken , forkedConfig . ManagementToken ) ;
116+ }
117+
118+ [ Fact ]
119+ public void Fork_SharedConfigurationReference ( )
120+ {
121+ // Arrange
122+ var parentClient = CreateClientWithTimeline ( ) ;
123+ var parentConfig = parentClient . GetLivePreviewConfig ( ) ;
124+ parentConfig . PreviewResponse = CreateMockPreviewResponse ( ) ;
125+
126+ // Act
127+ var forkedClient = parentClient . Fork ( ) ;
128+ var forkedConfig = forkedClient . GetLivePreviewConfig ( ) ;
129+
130+ // Assert - PreviewResponse is shared reference (memory efficient)
131+ Assert . Same ( parentConfig . PreviewResponse , forkedConfig . PreviewResponse ) ;
132+ }
133+
134+ [ Fact ]
135+ public void Fork_MultipleLevels_IndependentContexts ( )
136+ {
137+ // Arrange
138+ var level1Client = CreateClientWithTimeline ( ) ;
139+ level1Client . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-11-29T10:00:00.000Z" ;
140+
141+ // Act
142+ var level2Client = level1Client . Fork ( ) ;
143+ level2Client . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-11-29T12:00:00.000Z" ;
144+
145+ var level3Client = level2Client . Fork ( ) ;
146+ level3Client . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-11-29T14:00:00.000Z" ;
147+
148+ // Assert - Each level maintains its own timeline
149+ Assert . Equal ( "2024-11-29T10:00:00.000Z" , level1Client . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
150+ Assert . Equal ( "2024-11-29T12:00:00.000Z" , level2Client . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
151+ Assert . Equal ( "2024-11-29T14:00:00.000Z" , level3Client . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
152+
153+ // Assert - All are independent instances
154+ AssertClientsAreIndependent ( level1Client , level2Client ) ;
155+ AssertClientsAreIndependent ( level2Client , level3Client ) ;
156+ AssertClientsAreIndependent ( level1Client , level3Client ) ;
157+ }
158+
159+ [ Fact ]
160+ public void Fork_ParallelModifications_IsolatedChanges ( )
161+ {
162+ // Arrange
163+ var parentClient = CreateClientWithTimeline ( ) ;
164+ var fork1 = parentClient . Fork ( ) ;
165+ var fork2 = parentClient . Fork ( ) ;
166+
167+ // Act - Modify each fork independently
168+ fork1 . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-11-29T08:00:00.000Z" ;
169+ fork1 . GetLivePreviewConfig ( ) . ReleaseId = "fork1_release" ;
170+
171+ fork2 . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-11-29T16:00:00.000Z" ;
172+ fork2 . GetLivePreviewConfig ( ) . ReleaseId = "fork2_release" ;
173+
174+ // Assert - Changes are isolated
175+ Assert . Equal ( "2024-11-29T08:00:00.000Z" , fork1 . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
176+ Assert . Equal ( "fork1_release" , fork1 . GetLivePreviewConfig ( ) . ReleaseId ) ;
177+
178+ Assert . Equal ( "2024-11-29T16:00:00.000Z" , fork2 . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
179+ Assert . Equal ( "fork2_release" , fork2 . GetLivePreviewConfig ( ) . ReleaseId ) ;
180+
181+ // Parent client should maintain its original state
182+ Assert . Equal ( "2024-11-29T14:30:00.000Z" , parentClient . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
183+ Assert . Equal ( "test_release_123" , parentClient . GetLivePreviewConfig ( ) . ReleaseId ) ;
184+ }
185+
186+ [ Fact ]
187+ public void Fork_PreservesPlugins ( )
188+ {
189+ // Arrange
190+ var parentClient = CreateClientWithTimeline ( ) ;
191+ var mockHandler = new TimelineMockHttpHandler ( ) . ForSuccessfulLivePreview ( ) ;
192+ parentClient . Plugins . Add ( mockHandler ) ;
193+
194+ // Act
195+ var forkedClient = parentClient . Fork ( ) ;
196+
197+ // Assert - Plugins collection exists but fork doesn't share plugin instances
198+ Assert . NotNull ( forkedClient . Plugins ) ;
199+ Assert . Empty ( forkedClient . Plugins ) ; // Fork starts with empty plugins (isolated)
200+ }
201+
202+ [ Fact ]
203+ public async Task Fork_IndependentLivePreviewOperations ( )
204+ {
205+ // Arrange
206+ var parentClient = CreateClientWithLivePreview ( ) ;
207+ var fork1 = parentClient . Fork ( ) ;
208+ var fork2 = parentClient . Fork ( ) ;
209+
210+ // Set up different timeline contexts
211+ var query1 = CreateLivePreviewQuery ( previewTimestamp : "2024-11-29T08:00:00.000Z" ) ;
212+ var query2 = CreateLivePreviewQuery ( previewTimestamp : "2024-11-29T16:00:00.000Z" ) ;
213+
214+ // Act
215+ await fork1 . LivePreviewQueryAsync ( query1 ) ;
216+ await fork2 . LivePreviewQueryAsync ( query2 ) ;
217+
218+ // Assert - Each fork maintains its own timeline context
219+ Assert . Equal ( "2024-11-29T08:00:00.000Z" , fork1 . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
220+ Assert . Equal ( "2024-11-29T16:00:00.000Z" , fork2 . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
221+
222+ // Parent should be unaffected
223+ Assert . Null ( parentClient . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
224+ }
225+
226+ #endregion
227+
228+ #region Negative Test Cases
229+
230+ [ Fact ]
231+ public void Fork_WithNullLivePreviewConfig_HandlesGracefully ( )
232+ {
233+ // Arrange
234+ var parentClient = CreateClient ( ) ; // Client without LivePreview
235+ SetInternalProperty ( parentClient , "LivePreviewConfig" , null ) ;
236+
237+ // Act & Assert - Should not throw
238+ var forkedClient = parentClient . Fork ( ) ;
239+
240+ Assert . NotNull ( forkedClient ) ;
241+ // Verify the fork handles null config appropriately
242+ var forkedConfig = forkedClient . GetLivePreviewConfig ( ) ;
243+ Assert . NotNull ( forkedConfig ) ; // Should create a new config if parent was null
244+ }
245+
246+ [ Fact ]
247+ public void Fork_WithCorruptedConfiguration_CreatesValidFork ( )
248+ {
249+ // Arrange
250+ var parentClient = CreateClientWithTimeline ( ) ;
251+ var config = parentClient . GetLivePreviewConfig ( ) ;
252+
253+ // Corrupt some configuration properties
254+ config . Host = null ;
255+ config . PreviewTimestamp = "invalid-timestamp-format" ;
256+
257+ // Act & Assert - Should not throw
258+ var forkedClient = parentClient . Fork ( ) ;
259+ var forkedConfig = forkedClient . GetLivePreviewConfig ( ) ;
260+
261+ Assert . NotNull ( forkedClient ) ;
262+ Assert . NotNull ( forkedConfig ) ;
263+ Assert . Equal ( "invalid-timestamp-format" , forkedConfig . PreviewTimestamp ) ; // Corruption is copied but doesn't break fork
264+ }
265+
266+ [ Fact ]
267+ public void Fork_AfterParentModification_IsolatesChanges ( )
268+ {
269+ // Arrange
270+ var parentClient = CreateClientWithTimeline ( ) ;
271+ var originalTimestamp = parentClient . GetLivePreviewConfig ( ) . PreviewTimestamp ;
272+
273+ // Act - Create fork, then modify parent
274+ var forkedClient = parentClient . Fork ( ) ;
275+ parentClient . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-12-01T00:00:00.000Z" ;
276+
277+ // Assert - Fork maintains original timestamp
278+ Assert . Equal ( originalTimestamp , forkedClient . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
279+ Assert . Equal ( "2024-12-01T00:00:00.000Z" , parentClient . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
280+ }
281+
282+ [ Fact ]
283+ public void Fork_WithLargeNumberOfForks_MaintainsPerformance ( )
284+ {
285+ // Arrange
286+ var parentClient = CreateClientWithTimeline ( ) ;
287+ var numberOfForks = 1000 ;
288+ var forks = new ContentstackClient [ numberOfForks ] ;
289+
290+ // Act - Measure fork creation time
291+ var startTime = DateTime . UtcNow ;
292+
293+ for ( int i = 0 ; i < numberOfForks ; i ++ )
294+ {
295+ forks [ i ] = parentClient . Fork ( ) ;
296+ forks [ i ] . GetLivePreviewConfig ( ) . PreviewTimestamp = $ "2024-11-{ ( i % 12 ) + 1 : D2} -01T00:00:00.000Z";
297+ }
298+
299+ var duration = DateTime . UtcNow - startTime ;
300+
301+ // Assert - Fork creation should be fast (under 1 second for 1000 forks)
302+ Assert . True ( duration . TotalSeconds < 1.0 , $ "Fork creation took { duration . TotalMilliseconds } ms for { numberOfForks } forks") ;
303+
304+ // Verify all forks are independent
305+ for ( int i = 0 ; i < Math . Min ( 10 , numberOfForks ) ; i ++ ) // Check first 10 for performance
306+ {
307+ AssertClientsAreIndependent ( parentClient , forks [ i ] ) ;
308+ }
309+ }
310+
311+ #endregion
312+
313+ #region Edge Cases
314+
315+ [ Fact ]
316+ public void Fork_WithEmptyHeaders_HandlesCorrectly ( )
317+ {
318+ // Arrange
319+ var parentClient = CreateClientWithTimeline ( ) ;
320+ SetInternalField ( parentClient , "_LocalHeaders" , new Dictionary < string , object > ( ) ) ;
321+
322+ // Act
323+ var forkedClient = parentClient . Fork ( ) ;
324+
325+ // Assert
326+ Assert . NotNull ( forkedClient ) ;
327+ var forkedHeaders = GetInternalField < Dictionary < string , object > > ( forkedClient , "_LocalHeaders" ) ;
328+ Assert . NotNull ( forkedHeaders ) ;
329+ }
330+
331+ [ Fact ]
332+ public void Fork_WithNullHeaders_HandlesCorrectly ( )
333+ {
334+ // Arrange
335+ var parentClient = CreateClientWithTimeline ( ) ;
336+ SetInternalField ( parentClient , "_LocalHeaders" , null ) ;
337+
338+ // Act & Assert - Should not throw
339+ var forkedClient = parentClient . Fork ( ) ;
340+ Assert . NotNull ( forkedClient ) ;
341+ }
342+
343+ [ Fact ]
344+ public void Fork_RecursiveForkModification_MaintainsIsolation ( )
345+ {
346+ // Arrange
347+ var level1 = CreateClientWithTimeline ( ) ;
348+ level1 . GetLivePreviewConfig ( ) . ReleaseId = "level1_release" ;
349+
350+ var level2 = level1 . Fork ( ) ;
351+ level2 . GetLivePreviewConfig ( ) . ReleaseId = "level2_release" ;
352+
353+ var level3 = level2 . Fork ( ) ;
354+ level3 . GetLivePreviewConfig ( ) . ReleaseId = "level3_release" ;
355+
356+ // Act - Modify level2 after level3 is created
357+ level2 . GetLivePreviewConfig ( ) . PreviewTimestamp = "2024-11-30T00:00:00.000Z" ;
358+
359+ // Assert - Level3 should not be affected by level2 changes
360+ Assert . Equal ( "level1_release" , level1 . GetLivePreviewConfig ( ) . ReleaseId ) ;
361+ Assert . Equal ( "level2_release" , level2 . GetLivePreviewConfig ( ) . ReleaseId ) ;
362+ Assert . Equal ( "level3_release" , level3 . GetLivePreviewConfig ( ) . ReleaseId ) ;
363+
364+ Assert . Equal ( "2024-11-30T00:00:00.000Z" , level2 . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
365+ Assert . NotEqual ( "2024-11-30T00:00:00.000Z" , level3 . GetLivePreviewConfig ( ) . PreviewTimestamp ) ;
366+ }
367+
368+ #endregion
369+ }
370+ }
0 commit comments