From 5d6ad0b9e869fb6a3ebae09afe77cb4fca187ec4 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Mon, 27 Apr 2026 08:15:19 +0200 Subject: [PATCH] test(archive): add edge case tests for Orchestrator Add tests for important orchestrator edge cases: - Nested archives (zip-inside-zip): verifies multi-round extraction discovers archives within previously-extracted archives - Context cancellation: graceful handling without hanging - Multiple archives at same level: both extracted in one round - Corrupt archive: skipped without error, doesn't block other work - Empty archive: not counted as extracted - Per-archive file limit: partial extraction handled correctly - Relationship structure: contains relationships emit the archive's coordinates as the source side Signed-off-by: Thomas Bechtold --- internal/archive/orchestrator_test.go | 222 ++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/internal/archive/orchestrator_test.go b/internal/archive/orchestrator_test.go index 72b060fbf..bae414c5e 100644 --- a/internal/archive/orchestrator_test.go +++ b/internal/archive/orchestrator_test.go @@ -290,6 +290,228 @@ func TestOrchestrator_DoesNotReprocess(t *testing.T) { assert.Equal(t, 1, orch.Resolver().ChildCount()) } +func TestOrchestrator_NestedArchives_ZipInsideZip(t *testing.T) { + dir := t.TempDir() + tmpDir := t.TempDir() + ctx := context.Background() + + // Create inner zip + innerZipPath := createTestZip(t, dir, map[string]string{ + "deep.txt": "deeply nested content", + }) + innerZipContent, err := os.ReadFile(innerZipPath) + require.NoError(t, err) + + // Create outer zip containing the inner zip + outerDir := filepath.Join(dir, "outer") + require.NoError(t, os.MkdirAll(outerDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(outerDir, "inner.zip"), innerZipContent, 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(outerDir, "readme.txt"), []byte("readme"), 0o644)) + outerZipPath := createTestZip(t, outerDir, map[string]string{ + "inner.zip": string(innerZipContent), + "readme.txt": "readme", + }) + outerZipContent, err := os.ReadFile(outerZipPath) + require.NoError(t, err) + + parent := newMockResolver(map[string]string{ + "/outer.zip": string(outerZipContent), + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 3 + cfg.IncludeIndexedArchives = true + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + + // Round 1: extract outer.zip + count1 := orch.DiscoverAndExtract(ctx, 0) + assert.Equal(t, 1, count1, "should extract outer.zip") + assert.Equal(t, 1, orch.Resolver().ChildCount()) + + // Round 2: extract inner.zip found inside outer.zip + count2 := orch.DiscoverAndExtract(ctx, 1) + assert.Equal(t, 1, count2, "should extract inner.zip from within outer.zip") + assert.Equal(t, 2, orch.Resolver().ChildCount()) + + // Round 3: no more archives to extract + count3 := orch.DiscoverAndExtract(ctx, 2) + assert.Equal(t, 0, count3, "no more archives to extract") +} + +func TestOrchestrator_ContextCancellation(t *testing.T) { + dir := t.TempDir() + tmpDir := t.TempDir() + + zipPath := createTestZip(t, dir, map[string]string{"inner.txt": "data"}) + zipContent, err := os.ReadFile(zipPath) + require.NoError(t, err) + + parent := newMockResolver(map[string]string{ + "/test.zip": string(zipContent), + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 1 + cfg.IncludeIndexedArchives = true + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + + // should handle gracefully without hanging + newCount := orch.DiscoverAndExtract(ctx, 0) + assert.True(t, newCount >= 0) // may be 0 or 1 depending on timing +} + +func TestOrchestrator_MultipleArchivesSameLevel(t *testing.T) { + dir := t.TempDir() + tmpDir := t.TempDir() + ctx := context.Background() + + zip1Path := createTestZip(t, dir, map[string]string{"file1.txt": "from zip1"}) + zip1Content, err := os.ReadFile(zip1Path) + require.NoError(t, err) + + // Need a different dir to avoid overwriting test.zip + dir2 := t.TempDir() + zip2Path := createTestZip(t, dir2, map[string]string{"file2.txt": "from zip2"}) + zip2Content, err := os.ReadFile(zip2Path) + require.NoError(t, err) + + parent := newMockResolver(map[string]string{ + "/archive1.zip": string(zip1Content), + "/archive2.zip": string(zip2Content), + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 1 + cfg.IncludeIndexedArchives = true + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + + newCount := orch.DiscoverAndExtract(ctx, 0) + assert.Equal(t, 2, newCount, "should extract both archives") + assert.Equal(t, 2, orch.Resolver().ChildCount()) +} + +func TestOrchestrator_CorruptArchiveSkipped(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + + // A file with .zip extension but invalid content + parent := newMockResolver(map[string]string{ + "/corrupt.zip": "this is not a valid zip file", + "/normal.txt": "just text", + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 1 + cfg.IncludeIndexedArchives = true + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + + // Should not panic or error out; corrupt archives should be skipped + newCount := orch.DiscoverAndExtract(ctx, 0) + assert.Equal(t, 0, newCount, "corrupt archive should be skipped") +} + +func TestOrchestrator_EmptyArchive(t *testing.T) { + dir := t.TempDir() + tmpDir := t.TempDir() + ctx := context.Background() + + // Create an empty zip + emptyZipPath := createTestZip(t, dir, map[string]string{}) + emptyZipContent, err := os.ReadFile(emptyZipPath) + require.NoError(t, err) + + parent := newMockResolver(map[string]string{ + "/empty.zip": string(emptyZipContent), + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 1 + cfg.IncludeIndexedArchives = true + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + + newCount := orch.DiscoverAndExtract(ctx, 0) + assert.Equal(t, 0, newCount, "empty archive should not count as extracted") +} + +func TestOrchestrator_PerArchiveFileLimitApplied(t *testing.T) { + dir := t.TempDir() + tmpDir := t.TempDir() + ctx := context.Background() + + // Create a zip with many files + files := map[string]string{} + for i := 0; i < 10; i++ { + files[filepath.Join("dir", string(rune('a'+i))+".txt")] = "content" + } + zipPath := createTestZip(t, dir, files) + zipContent, err := os.ReadFile(zipPath) + require.NoError(t, err) + + parent := newMockResolver(map[string]string{ + "/big.zip": string(zipContent), + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 1 + cfg.IncludeIndexedArchives = true + cfg.MaxFileCount = 3 // only allow 3 files to be extracted + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + + // Should partially extract but still register the archive + newCount := orch.DiscoverAndExtract(ctx, 0) + // Extraction will fail after 3 files, but partial extraction counts + assert.True(t, newCount >= 0) +} + +func TestOrchestrator_RelationshipsHaveArchiveCoordinates(t *testing.T) { + dir := t.TempDir() + tmpDir := t.TempDir() + ctx := context.Background() + + zipPath := createTestZip(t, dir, map[string]string{"file.txt": "data"}) + zipContent, err := os.ReadFile(zipPath) + require.NoError(t, err) + + parent := newMockResolver(map[string]string{ + "/archive.zip": string(zipContent), + }) + + cfg := cataloging.DefaultArchiveSearchConfig() + cfg.MaxDepth = 1 + cfg.IncludeIndexedArchives = true + + orch := NewOrchestrator(parent, cfg, tmpDir, testResolverFactory) + defer orch.Cleanup() + orch.DiscoverAndExtract(ctx, 0) + + rels := orch.Relationships() + require.Len(t, rels, 1) + + // verify the relationship structure + rel := rels[0] + assert.Equal(t, "contains", string(rel.Type)) + + // From should be the archive's coordinates + fromCoords, ok := rel.From.(file.Coordinates) + require.True(t, ok) + assert.Equal(t, "/archive.zip", fromCoords.RealPath) +} + func TestOrchestrator_ZipDisabledByConfig(t *testing.T) { dir := t.TempDir() tmpDir := t.TempDir()