Skip to content

Commit 92fffc0

Browse files
committed
v0.21.0 Implemented Export E2E tests
1 parent 83197a5 commit 92fffc0

File tree

11 files changed

+1984
-46
lines changed

11 files changed

+1984
-46
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.20.0
1+
0.21.0

Directory.Build.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>0.20.0</Version>
4-
<AssemblyVersion>0.20.0.0</AssemblyVersion>
5-
<FileVersion>0.20.0.0</FileVersion>
3+
<Version>0.21.0</Version>
4+
<AssemblyVersion>0.21.0.0</AssemblyVersion>
5+
<FileVersion>0.21.0.0</FileVersion>
66
</PropertyGroup>
77
</Project>

Neo4jExport.Tests/EndToEnd/ExportTests/BasicExportTests.fs

Lines changed: 490 additions & 0 deletions
Large diffs are not rendered by default.

Neo4jExport.Tests/EndToEnd/ExportTests/DataTypeTests.fs

Lines changed: 715 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
module Neo4jExport.Tests.EndToEnd.ExportTests.ErrorHandlingTests
2+
3+
open System
4+
open System.IO
5+
open System.Collections.Concurrent
6+
open Expecto
7+
open Swensen.Unquote
8+
open System.Text.Json
9+
open Neo4jExport
10+
open Neo4jExport.Workflow
11+
open Neo4jExport.Tests.EndToEnd.Infrastructure.ContainerFixtures
12+
open Neo4jExport.Tests.EndToEnd.Infrastructure.ContainerLifecycle
13+
open Neo4jExport.Tests.EndToEnd.Infrastructure.TestDataManagement
14+
open Neo4jExport.Tests.Helpers.TestHelpers
15+
16+
[<Tests>]
17+
let tests =
18+
testList
19+
"Error Handling E2E Tests"
20+
[
21+
22+
// Test 1: Invalid credentials
23+
testAsync "handles authentication errors correctly" {
24+
let config = createTestConfig ()
25+
26+
let invalidConfig =
27+
{ config with
28+
Password = "wrong-password" }
29+
30+
// Create application context
31+
let context =
32+
{ CancellationTokenSource = new System.Threading.CancellationTokenSource()
33+
TempFiles = System.Collections.Concurrent.ConcurrentBag<string>()
34+
ActiveProcesses = System.Collections.Concurrent.ConcurrentBag<System.Diagnostics.Process>() }
35+
36+
match! Workflow.runExport context invalidConfig with
37+
| Ok() -> failtest "Expected authentication error"
38+
| Error err ->
39+
match err with
40+
| AuthenticationError _ -> () // Expected
41+
| _ -> failtest $"Expected AuthenticationError but got: {err}"
42+
}
43+
44+
// Test 2: Invalid output directory
45+
testAsync "handles invalid output paths correctly" {
46+
let config = createTestConfig ()
47+
48+
let invalidConfig =
49+
{ config with
50+
OutputDirectory = "/invalid/path/that/does/not/exist" }
51+
52+
// Create application context
53+
let context =
54+
{ CancellationTokenSource = new System.Threading.CancellationTokenSource()
55+
TempFiles = System.Collections.Concurrent.ConcurrentBag<string>()
56+
ActiveProcesses = System.Collections.Concurrent.ConcurrentBag<System.Diagnostics.Process>() }
57+
58+
match! Workflow.runExport context invalidConfig with
59+
| Ok() -> failtest "Expected file system error"
60+
| Error err ->
61+
match err with
62+
| FileSystemError _ -> () // Expected
63+
| _ -> failtest $"Expected FileSystemError but got: {err}"
64+
}
65+
66+
// Test 3: Connection timeout
67+
testAsync "handles connection timeouts correctly" {
68+
let config = createTestConfig ()
69+
70+
let timeoutConfig =
71+
{ config with
72+
Uri = Uri("bolt://non-existent-host:7687") }
73+
74+
// Create application context
75+
let context =
76+
{ CancellationTokenSource = new System.Threading.CancellationTokenSource()
77+
TempFiles = System.Collections.Concurrent.ConcurrentBag<string>()
78+
ActiveProcesses = System.Collections.Concurrent.ConcurrentBag<System.Diagnostics.Process>() }
79+
80+
match! Workflow.runExport context timeoutConfig with
81+
| Ok() -> failtest "Expected connection error"
82+
| Error err ->
83+
match err with
84+
| ConnectionError _ -> () // Expected
85+
| _ -> failtest $"Expected ConnectionError but got: {err}"
86+
}
87+
88+
// Test 4: Cancellation handling
89+
testAsync "handles cancellation gracefully" {
90+
do!
91+
ContainerFixture.withContainerInfo ContainerConfig.defaultConfig (fun containerInfo ->
92+
async {
93+
do!
94+
TestExecution.withSeededData containerInfo.Driver SimpleGraph Medium (fun () ->
95+
async {
96+
// Create a cancellation token that will cancel after a short delay
97+
let context =
98+
{ CancellationTokenSource = new System.Threading.CancellationTokenSource()
99+
TempFiles = System.Collections.Concurrent.ConcurrentBag<string>()
100+
ActiveProcesses =
101+
System.Collections.Concurrent.ConcurrentBag<System.Diagnostics.Process>() }
102+
103+
// Schedule cancellation after 500ms (during export)
104+
context.CancellationTokenSource.CancelAfter(500)
105+
106+
let config = createTestConfig ()
107+
108+
let exportConfig =
109+
{ config with
110+
Uri = Uri(containerInfo.ConnectionString)
111+
User = "neo4j"
112+
Password = containerInfo.Password
113+
OutputDirectory =
114+
Path.Combine(Path.GetTempPath(), $"cancel-test-{Guid.NewGuid()}") }
115+
116+
// Create output directory
117+
Directory.CreateDirectory(exportConfig.OutputDirectory)
118+
|> ignore
119+
120+
try
121+
// Run export that should be cancelled
122+
let! result = Workflow.runExport context exportConfig
123+
124+
match result with
125+
| Ok() ->
126+
// If we get here, the export completed before cancellation
127+
// This is OK for small datasets
128+
()
129+
| Error err ->
130+
// We expect a cancellation-related error
131+
match err with
132+
| TimeoutError _ -> () // Cancellation manifests as timeout
133+
| ExportError(msg, _) when msg.Contains("cancel") -> () // Or export error
134+
| _ -> ()
135+
136+
// Verify no partial files remain
137+
let exportFiles =
138+
Directory.GetFiles(exportConfig.OutputDirectory, "*.jsonl")
139+
140+
if exportFiles.Length > 0 then
141+
// If a file exists, it should be valid JSONL
142+
let firstLine =
143+
File.ReadLines(exportFiles.[0]) |> Seq.tryHead
144+
145+
match firstLine with
146+
| Some line ->
147+
// Should be valid JSON
148+
use _ = JsonDocument.Parse(line)
149+
()
150+
| None -> ()
151+
152+
finally
153+
// Clean up test directory
154+
if Directory.Exists(exportConfig.OutputDirectory) then
155+
Directory.Delete(exportConfig.OutputDirectory, true)
156+
})
157+
})
158+
}
159+
160+
// Test 5: Disk space exhaustion
161+
testAsync "handles disk space errors correctly" {
162+
// This test would require mocking or a very small disk quota
163+
// For now, verify the error type exists and can be constructed
164+
let error = DiskSpaceError(1000L, 500L) // Required 1000 bytes, only 500 available
165+
166+
test
167+
<@
168+
match error with
169+
| DiskSpaceError _ -> true
170+
| _ -> false
171+
@>
172+
}
173+
174+
// Test 6: Error record generation
175+
testAsync "generates error records in output" {
176+
do!
177+
ContainerFixture.withContainerInfo ContainerConfig.defaultConfig (fun containerInfo ->
178+
async {
179+
do!
180+
TestExecution.withSeededData containerInfo.Driver ComplexTypes Small (fun () ->
181+
async {
182+
// Run export
183+
let uri =
184+
Uri(containerInfo.ConnectionString)
185+
186+
match! Fixtures.runExportWithMetrics uri "neo4j" containerInfo.Password with
187+
| Ok(_metrics, exportFile) ->
188+
// Parse file
189+
let (metadata, records) =
190+
ExportTestUtils.parseExportFile exportFile
191+
192+
// Find error/warning records
193+
let errorRecords =
194+
records
195+
|> Array.filter (fun r ->
196+
ExportTestUtils.isRecordType "error" r
197+
|| ExportTestUtils.isRecordType "warning" r)
198+
199+
// Should have error/warning records for unsupported types
200+
test <@ errorRecords.Length > 0 @>
201+
202+
if errorRecords.Length > 0 then
203+
let firstError = errorRecords.[0]
204+
205+
// Verify error record structure
206+
let hasType =
207+
firstError.TryGetProperty("type") |> fst
208+
209+
test <@ hasType @>
210+
211+
let hasTimestamp =
212+
firstError.TryGetProperty("timestamp") |> fst
213+
214+
test <@ hasTimestamp @>
215+
216+
let hasMessage =
217+
firstError.TryGetProperty("message") |> fst
218+
219+
test <@ hasMessage @>
220+
221+
// Timestamp should be valid ISO format
222+
let timestamp =
223+
firstError.GetProperty("timestamp").GetString()
224+
225+
let parsed = DateTime.TryParse(timestamp)
226+
test <@ fst parsed = true @>
227+
228+
// Message should be informative
229+
let message =
230+
firstError.GetProperty("message").GetString()
231+
232+
test <@ message.Length > 0 @>
233+
234+
// Check metadata includes error_summary
235+
let hasErrorSummary =
236+
metadata.TryGetProperty("error_summary") |> fst
237+
238+
test <@ hasErrorSummary @>
239+
240+
let errorSummary =
241+
metadata.GetProperty("error_summary")
242+
243+
// Verify error summary fields
244+
let hasErrorCount =
245+
errorSummary.TryGetProperty("error_count") |> fst
246+
247+
test <@ hasErrorCount @>
248+
249+
let hasWarningCount =
250+
errorSummary.TryGetProperty("warning_count")
251+
|> fst
252+
253+
test <@ hasWarningCount @>
254+
255+
let hasErrors =
256+
errorSummary.TryGetProperty("has_errors") |> fst
257+
258+
test <@ hasErrors @>
259+
260+
let errorCount =
261+
errorSummary.GetProperty("error_count").GetInt64()
262+
263+
let warningCount =
264+
errorSummary.GetProperty("warning_count").GetInt64()
265+
266+
let hasErrors =
267+
errorSummary.GetProperty("has_errors").GetBoolean()
268+
269+
// Counts should match actual error records
270+
let actualErrors =
271+
errorRecords
272+
|> Array.filter (ExportTestUtils.isRecordType "error")
273+
274+
let actualWarnings =
275+
errorRecords
276+
|> Array.filter (ExportTestUtils.isRecordType "warning")
277+
278+
test <@ int64 actualErrors.Length = errorCount @>
279+
test <@ int64 actualWarnings.Length = warningCount @>
280+
test <@ hasErrors = (errorCount > 0L) @>
281+
282+
| Error err -> failtest $"Export failed: {err}"
283+
})
284+
})
285+
} ]

0 commit comments

Comments
 (0)