diff --git a/src/Elastic.Codex/Elastic.Codex.csproj b/src/Elastic.Codex/Elastic.Codex.csproj index bb2e313e3..f6fe523b0 100644 --- a/src/Elastic.Codex/Elastic.Codex.csproj +++ b/src/Elastic.Codex/Elastic.Codex.csproj @@ -12,6 +12,10 @@ true + + + + diff --git a/src/Elastic.Codex/Sourcing/CodexCloneService.cs b/src/Elastic.Codex/Sourcing/CodexCloneService.cs index 531b0fd20..b39c1581d 100644 --- a/src/Elastic.Codex/Sourcing/CodexCloneService.cs +++ b/src/Elastic.Codex/Sourcing/CodexCloneService.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using System.Security; using Elastic.Documentation.Configuration.Codex; using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; @@ -268,9 +269,9 @@ await context.WriteFileSystem.File.WriteAllTextAsync( return found; } } - catch (UnauthorizedAccessException) + catch (Exception ex) when (ex is UnauthorizedAccessException or SecurityException) { - // Skip directories we can't access + // Skip directories we can't access (including ScopedFileSystem-blocked hidden dirs) } return null; diff --git a/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs b/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs new file mode 100644 index 000000000..91961955f --- /dev/null +++ b/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs @@ -0,0 +1,89 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Codex.Sourcing; +using Elastic.Documentation.Configuration; +using Nullean.ScopedFileSystem; + +namespace Elastic.Documentation.Navigation.Tests.Codex; + +public class FindDocsetFileTests +{ + private static readonly string RepoRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, "repo"); + + private static ScopedFileSystem CreateScopedFs(MockFileSystem mockFs) => + FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs); + + [Fact] + public void StandardPath_Found() + { + var mockFs = new MockFileSystem(new Dictionary + { + { Path.Join(RepoRoot, "docs/docset.yml"), new MockFileData("project: test") } + }); + + var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot)); + + result.Should().NotBeNull(); + result.Name.Should().Be("docset.yml"); + } + + [Fact] + public void NonStandardPath_FoundViaRecursion() + { + var mockFs = new MockFileSystem(new Dictionary + { + { Path.Join(RepoRoot, "docs-codex/docset.yml"), new MockFileData("project: test") } + }); + + var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot)); + + result.Should().NotBeNull(); + result.Name.Should().Be("docset.yml"); + } + + [Fact] + public void HiddenDirectory_SkippedByScopedFileSystem() + { + var mockFs = new MockFileSystem(new Dictionary + { + { Path.Join(RepoRoot, ".github/workflows/ci.yml"), new MockFileData("name: CI") }, + { Path.Join(RepoRoot, "docs-codex/docset.yml"), new MockFileData("project: test") } + }); + var scopedFs = CreateScopedFs(mockFs); + + var result = CodexCloneService.FindDocsetFile(scopedFs, scopedFs.DirectoryInfo.New(RepoRoot)); + + result.Should().NotBeNull(); + result.Name.Should().Be("docset.yml"); + } + + [Fact] + public void NoDocset_ReturnsNull() + { + var mockFs = new MockFileSystem(new Dictionary + { + { Path.Join(RepoRoot, "src/main.py"), new MockFileData("print('hello')") } + }); + + var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot)); + + result.Should().BeNull(); + } + + [Fact] + public void NodeModules_Skipped() + { + var mockFs = new MockFileSystem(new Dictionary + { + { Path.Join(RepoRoot, "node_modules/some-pkg/docset.yml"), new MockFileData("project: fake") } + }); + + var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot)); + + result.Should().BeNull(); + } +}