/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Server.FileSystem; using Opc.Ua.Server.Tests.NodeManager; using Opc.Ua.Tests; namespace Opc.Ua.Server.Tests.FileSystem { /// /// Deterministic offline tests for that drive /// the FileDirectoryType method handlers (CreateFile / CreateDirectory / /// DeleteFileSystemObject / MoveOrCopy) and browser population against a physical /// provider rooted at a temp directory. /// [TestFixture] [Category("FileSystem")] public class DirectoryObjectStateTests { private string m_root = null!; private ITelemetryContext m_telemetry = null!; private FileSystemNodeManager m_manager = null!; private ISystemContext m_context = null!; private NodeId m_sessionId; [SetUp] public void SetUp() { m_root = Path.Combine(Path.GetTempPath(), "dos-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(m_root); m_telemetry = NUnitTelemetryContext.Create(); UseProvider(new PhysicalFileSystemProvider(m_root, "TestMount")); } [TearDown] public void TearDown() { m_manager?.Dispose(); m_manager = null!; m_context = null!; try { if (Directory.Exists(m_root)) { Directory.Delete(m_root, recursive: true); } } catch (IOException) { // best-effort } } private void UseProvider(IFileSystemProvider provider) { m_manager?.Dispose(); Mock mockServer = DeterministicServerMock.Create(out _); mockServer.Setup(s => s.Telemetry).Returns(m_telemetry); m_manager = new FileSystemNodeManager(mockServer.Object, new ApplicationConfiguration(), provider); m_sessionId = new NodeId("directory-session", 0); var session = new Mock(); session.Setup(s => s.Id).Returns(m_sessionId); session.Setup(s => s.Identity).Returns(Mock.Of(i => i.TokenType == UserTokenType.UserName)); session.Setup(s => s.PreferredLocales).Returns([]); m_context = m_manager.SystemContext.Copy(session.Object); } private DirectoryObjectState CreateRootDirectory() { NodeId nodeId = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); return new DirectoryObjectState(m_context, nodeId, string.Empty, "Root", isRoot: true); } private SystemContext OrphanContext() { return new SystemContext(m_telemetry) { SystemHandle = null }; } [Test] public async Task CreateDirectoryCreatesDirectoryAndReturnsNodeIdAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateDirectoryMethodStateResult result = await state.CreateDirectory!.OnCallAsync!( m_context, state.CreateDirectory, state.NodeId, "newdir", CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(result.DirectoryNodeId, Is.Not.EqualTo(NodeId.Null)); Assert.That(Directory.Exists(Path.Combine(m_root, "newdir")), Is.True); } [Test] public async Task CreateDirectoryWithEmptyNameReturnsBadInvalidArgumentAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateDirectoryMethodStateResult result = await state.CreateDirectory!.OnCallAsync!( m_context, state.CreateDirectory, state.NodeId, string.Empty, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument)); } [Test] public async Task CreateDirectoryWithUnavailableManagerReturnsBadInvalidStateAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateDirectoryMethodStateResult result = await state.CreateDirectory!.OnCallAsync!( OrphanContext(), state.CreateDirectory, state.NodeId, "x", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState)); } [Test] public async Task CreateFileWithoutOpenCreatesFileAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( m_context, state.CreateFile, state.NodeId, "new.txt", false, CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(result.FileNodeId, Is.Not.EqualTo(NodeId.Null)); Assert.That(result.FileHandle, Is.Zero); Assert.That(File.Exists(Path.Combine(m_root, "new.txt")), Is.True); } [Test] public async Task CreateFileWithOpenReturnsFileHandleAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( m_context, state.CreateFile, state.NodeId, "opened.txt", true, CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(result.FileHandle, Is.GreaterThan(0u)); } [Test] public async Task CreateFileWithOpenWithoutSessionReturnsBadSessionIdInvalidAsync() { DirectoryObjectState state = CreateRootDirectory(); ISystemContext contextWithoutSession = m_manager.SystemContext.Copy(); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( contextWithoutSession, state.CreateFile, state.NodeId, "opened.txt", true, CancellationToken.None).ConfigureAwait(false); Assert.That( result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadSessionIdInvalid)); Assert.That(result.FileHandle, Is.Zero); Assert.That(File.Exists(Path.Combine(m_root, "opened.txt")), Is.False); } [Test] public async Task CreateFileWithOpenScopesFileHandleToCreatingSessionAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( m_context, state.CreateFile, state.NodeId, "scoped.txt", true, CancellationToken.None) .ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); FileHandle handle = m_manager.GetOrCreateHandle(result.FileNodeId, "scoped.txt")!; var otherSessionId = new NodeId("other-directory-session", 0); bool closedByOtherSession = handle.Close(otherSessionId, result.FileHandle); bool closedByCreatingSession = handle.Close(m_sessionId, result.FileHandle); Assert.That(closedByOtherSession, Is.False); Assert.That(closedByCreatingSession, Is.True); } [Test] public async Task CreateFileWithEmptyNameReturnsBadInvalidArgumentAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( m_context, state.CreateFile, state.NodeId, string.Empty, false, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument)); } [Test] public async Task CreateFileWithUnavailableManagerReturnsBadInvalidStateAsync() { DirectoryObjectState state = CreateRootDirectory(); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( OrphanContext(), state.CreateFile, state.NodeId, "x.txt", false, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState)); } [Test] public async Task DeleteFileSystemObjectDeletesFileAsync() { DirectoryObjectState state = CreateRootDirectory(); File.WriteAllText(Path.Combine(m_root, "del.txt"), "x"); NodeId target = FileSystemNodeId.BuildFile("del.txt", m_manager.NamespaceIndex); DeleteFileMethodStateResult result = await state.DeleteFileSystemObject!.OnCallAsync!( m_context, state.DeleteFileSystemObject, state.NodeId, target, CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(File.Exists(Path.Combine(m_root, "del.txt")), Is.False); } [Test] public async Task DeleteFileSystemObjectRejectsRootAsync() { DirectoryObjectState state = CreateRootDirectory(); NodeId target = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); DeleteFileMethodStateResult result = await state.DeleteFileSystemObject!.OnCallAsync!( m_context, state.DeleteFileSystemObject, state.NodeId, target, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadUserAccessDenied)); } [Test] public async Task DeleteFileSystemObjectWithNonFileSystemNodeReturnsBadNotFoundAsync() { DirectoryObjectState state = CreateRootDirectory(); var target = new NodeId(42); DeleteFileMethodStateResult result = await state.DeleteFileSystemObject!.OnCallAsync!( m_context, state.DeleteFileSystemObject, state.NodeId, target, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadNotFound)); } [Test] public async Task DeleteFileSystemObjectForMissingFileReturnsBadNotFoundAsync() { DirectoryObjectState state = CreateRootDirectory(); NodeId target = FileSystemNodeId.BuildFile("missing.txt", m_manager.NamespaceIndex); DeleteFileMethodStateResult result = await state.DeleteFileSystemObject!.OnCallAsync!( m_context, state.DeleteFileSystemObject, state.NodeId, target, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadNotFound)); } [Test] public async Task DeleteFileSystemObjectWithUnavailableManagerReturnsBadInvalidStateAsync() { DirectoryObjectState state = CreateRootDirectory(); NodeId target = FileSystemNodeId.BuildFile("del.txt", m_manager.NamespaceIndex); DeleteFileMethodStateResult result = await state.DeleteFileSystemObject!.OnCallAsync!( OrphanContext(), state.DeleteFileSystemObject, state.NodeId, target, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState)); } [Test] public async Task MoveOrCopyMovesFileAsync() { DirectoryObjectState state = CreateRootDirectory(); File.WriteAllText(Path.Combine(m_root, "src.txt"), "payload"); NodeId source = FileSystemNodeId.BuildFile("src.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, false, "moved.txt", CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(result.NewNodeId, Is.Not.EqualTo(NodeId.Null)); Assert.That(File.Exists(Path.Combine(m_root, "moved.txt")), Is.True); Assert.That(File.Exists(Path.Combine(m_root, "src.txt")), Is.False); } [Test] public async Task MoveOrCopyCopiesFileAsync() { DirectoryObjectState state = CreateRootDirectory(); File.WriteAllText(Path.Combine(m_root, "orig.txt"), "payload"); NodeId source = FileSystemNodeId.BuildFile("orig.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, true, "copy.txt", CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(File.Exists(Path.Combine(m_root, "copy.txt")), Is.True); Assert.That(File.Exists(Path.Combine(m_root, "orig.txt")), Is.True); } [Test] public async Task MoveOrCopyUsesSourceNameWhenNewNameEmptyAsync() { // MoveOrCopy is called on the directory that organizes the source. var state = new DirectoryObjectState(m_context, FileSystemNodeId.BuildDirectory("sub", m_manager.NamespaceIndex), "sub", "sub", isRoot: false); Directory.CreateDirectory(Path.Combine(m_root, "sub")); File.WriteAllText(Path.Combine(m_root, "sub", "keep.txt"), "payload"); NodeId source = FileSystemNodeId.BuildFile("sub/keep.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, true, string.Empty, CancellationToken.None).ConfigureAwait(false); Assert.That(ServiceResult.IsGood(result.ServiceResult), Is.True); Assert.That(File.Exists(Path.Combine(m_root, "keep.txt")), Is.True); } [Test] public async Task MoveOrCopyWithRootSourceReturnsBadInvalidArgumentAsync() { DirectoryObjectState state = CreateRootDirectory(); NodeId source = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, false, "x", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument)); } [Test] public async Task MoveOrCopyWithFileTargetReturnsBadInvalidArgumentAsync() { DirectoryObjectState state = CreateRootDirectory(); File.WriteAllText(Path.Combine(m_root, "src.txt"), "payload"); NodeId source = FileSystemNodeId.BuildFile("src.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildFile("target.txt", m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, false, "x", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidArgument)); } [Test] public async Task MoveOrCopyWithUnavailableManagerReturnsBadInvalidStateAsync() { DirectoryObjectState state = CreateRootDirectory(); NodeId source = FileSystemNodeId.BuildFile("src.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( OrphanContext(), state.MoveOrCopy, state.NodeId, source, targetDir, false, "x", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadInvalidState)); } [Test] public async Task CreateDirectoryOverExistingFileReturnsBadBrowseNameDuplicatedAsync() { DirectoryObjectState state = CreateRootDirectory(); File.WriteAllText(Path.Combine(m_root, "clash"), "x"); CreateDirectoryMethodStateResult result = await state.CreateDirectory!.OnCallAsync!( m_context, state.CreateDirectory, state.NodeId, "clash", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadBrowseNameDuplicated)); } [Test] public async Task CreateFileOverExistingDirectoryReturnsBadBrowseNameDuplicatedAsync() { DirectoryObjectState state = CreateRootDirectory(); Directory.CreateDirectory(Path.Combine(m_root, "clashdir")); CreateFileMethodStateResult result = await state.CreateFile!.OnCallAsync!( m_context, state.CreateFile, state.NodeId, "clashdir", false, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadBrowseNameDuplicated)); } [Test] public async Task DeleteFileSystemObjectWhenProviderDeniesDeleteReturnsBadAsync() { var provider = new Mock(MockBehavior.Strict); provider.SetupGet(p => p.MountName).Returns("TestMount"); provider.SetupGet(p => p.IsWritable).Returns(true); provider .Setup(p => p.GetEntryAsync("locked.txt", It.IsAny())) .Returns(new ValueTask(new FileSystemEntry( "locked.txt", "locked.txt", false, 0, true, DateTime.UtcNow, string.Empty))); provider .Setup(p => p.DeleteAsync("locked.txt", It.IsAny())) .Returns((_, _) => throw new IOException("locked")); UseProvider(provider.Object); DirectoryObjectState state = CreateRootDirectory(); NodeId target = FileSystemNodeId.BuildFile("locked.txt", m_manager.NamespaceIndex); DeleteFileMethodStateResult result = await state.DeleteFileSystemObject!.OnCallAsync!( m_context, state.DeleteFileSystemObject, state.NodeId, target, CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadUserAccessDenied)); provider.Verify( p => p.DeleteAsync("locked.txt", It.IsAny()), Times.Once); } [Test] public async Task MoveOrCopyMissingSourceReturnsBadNotFoundAsync() { DirectoryObjectState state = CreateRootDirectory(); NodeId source = FileSystemNodeId.BuildFile("ghost.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, false, "x.txt", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadNotFound)); } [Test] public async Task MoveOrCopyToExistingTargetReturnsBadBrowseNameDuplicatedAsync() { DirectoryObjectState state = CreateRootDirectory(); File.WriteAllText(Path.Combine(m_root, "src.txt"), "a"); File.WriteAllText(Path.Combine(m_root, "dest.txt"), "b"); NodeId source = FileSystemNodeId.BuildFile("src.txt", m_manager.NamespaceIndex); NodeId targetDir = FileSystemNodeId.BuildRoot(m_manager.NamespaceIndex); MoveOrCopyMethodStateResult result = await state.MoveOrCopy!.OnCallAsync!( m_context, state.MoveOrCopy, state.NodeId, source, targetDir, false, "dest.txt", CancellationToken.None).ConfigureAwait(false); Assert.That(result.ServiceResult.StatusCode.Code, Is.EqualTo(StatusCodes.BadBrowseNameDuplicated)); } [Test] public void CreateBrowserForRootAddsFileSystemParentReference() { DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Inverse, state.BrowseName, null, false); bool foundFileSystemParent = false; for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { if (reference.ReferenceTypeId == ReferenceTypeIds.HasComponent && reference.IsInverse && reference.TargetId == ObjectIds.FileSystem) { foundFileSystemParent = true; } } Assert.That(foundFileSystemParent, Is.True); } [Test] public void CreateBrowserForNestedDirectoryAddsParentReference() { NodeId nodeId = FileSystemNodeId.BuildDirectory("parent/child", m_manager.NamespaceIndex); var state = new DirectoryObjectState(m_context, nodeId, "parent/child", "child", isRoot: false); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Inverse, state.BrowseName, null, false); bool foundParent = false; for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { if (reference.ReferenceTypeId == ReferenceTypeIds.HasComponent && reference.IsInverse) { foundParent = true; } } Assert.That(foundParent, Is.True); } [Test] public void CreateBrowserEnumeratesImmediateFilesAndDirectories() { Directory.CreateDirectory(Path.Combine(m_root, "folder")); File.WriteAllText(Path.Combine(m_root, "a.txt"), "content"); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, QualifiedName.Null, null, false); List targets = GetTargetIds(browser); Assert.That(targets, Does.Contain(new ExpandedNodeId( FileSystemNodeId.BuildDirectory("folder", m_manager.NamespaceIndex)))); Assert.That(targets, Does.Contain(new ExpandedNodeId( FileSystemNodeId.BuildFile("a.txt", m_manager.NamespaceIndex)))); } [Test] public void CreateBrowserWithBrowseNameReturnsOnlyMatchingChild() { Directory.CreateDirectory(Path.Combine(m_root, "folder")); File.WriteAllText(Path.Combine(m_root, "a.txt"), "content"); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, new QualifiedName("a.txt", state.BrowseName.NamespaceIndex), null, false); List targets = GetTargetIds(browser); Assert.That(targets, Is.EqualTo( [ new ExpandedNodeId( FileSystemNodeId.BuildFile("a.txt", m_manager.NamespaceIndex)) ])); } [Test] public void CreateBrowserWithBrowseNameNamespaceMismatchReturnsNoChildren() { File.WriteAllText(Path.Combine(m_root, "a.txt"), "content"); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, new QualifiedName("a.txt", (ushort)(state.BrowseName.NamespaceIndex + 1)), null, false); Assert.That(GetTargetIds(browser), Is.Empty); } [Test] public void CreateBrowserSkipsProviderEnumerationForInternalOnlyAndWrongReferenceType() { File.WriteAllText(Path.Combine(m_root, "a.txt"), "content"); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser internalOnly = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, QualifiedName.Null, null, true); using INodeBrowser wrongReferenceType = state.CreateBrowser( m_context, null, ReferenceTypeIds.Organizes, true, BrowseDirection.Forward, QualifiedName.Null, null, false); Assert.That(GetTargetIds(internalOnly), Is.Empty); Assert.That(GetTargetIds(wrongReferenceType), Is.Empty); } [Test] public void CreateBrowserPropagatesProviderEnumerationFailure() { var provider = new ThrowingEnumerateProvider(); UseProvider(provider); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, QualifiedName.Null, null, false); IOException error = Assert.Throws(() => GetTargetIds(browser))!; Assert.That(error, Is.SameAs(provider.EnumerationFailure)); Assert.That(GetTargetIds(browser), Is.Empty); Assert.That(provider.EnumerationCount, Is.EqualTo(1)); } [Test] public async Task NextAsyncEnumeratesImmediateFilesAndDirectoriesAsync() { Directory.CreateDirectory(Path.Combine(m_root, "folder")); File.WriteAllText(Path.Combine(m_root, "a.txt"), "content"); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, QualifiedName.Null, null, false); List targets = await GetTargetIdsAsync(browser).ConfigureAwait(false); Assert.That(targets, Does.Contain(new ExpandedNodeId( FileSystemNodeId.BuildDirectory("folder", m_manager.NamespaceIndex)))); Assert.That(targets, Does.Contain(new ExpandedNodeId( FileSystemNodeId.BuildFile("a.txt", m_manager.NamespaceIndex)))); } [Test] public async Task NextAsyncPropagatesProviderEnumerationFailureAsync() { var provider = new ThrowingEnumerateProvider(); UseProvider(provider); DirectoryObjectState state = CreateRootDirectory(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, QualifiedName.Null, null, false); IOException error = Assert.ThrowsAsync( async () => await GetTargetIdsAsync(browser).ConfigureAwait(false))!; Assert.That(error, Is.SameAs(provider.EnumerationFailure)); Assert.That(await GetTargetIdsAsync(browser).ConfigureAwait(false), Is.Empty); Assert.That(provider.EnumerationCount, Is.EqualTo(1)); } [Test] public void NextAsyncPropagatesCancellationOfTheProviderEnumeration() { UseProvider(new ThrowingEnumerateProvider()); DirectoryObjectState state = CreateRootDirectory(); using var cts = new CancellationTokenSource(); cts.Cancel(); using INodeBrowser browser = state.CreateBrowser( m_context, null, ReferenceTypeIds.HasComponent, true, BrowseDirection.Forward, QualifiedName.Null, null, false); Assert.ThrowsAsync( async () => await browser.NextAsync(cts.Token).ConfigureAwait(false)); } private static List GetTargetIds(INodeBrowser browser) { var targets = new List(); for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { if (!reference.IsInverse) { targets.Add(reference.TargetId); } } return targets; } private static async Task> GetTargetIdsAsync(INodeBrowser browser) { var targets = new List(); for (IReference? reference = await browser.NextAsync().ConfigureAwait(false); reference != null; reference = await browser.NextAsync().ConfigureAwait(false)) { if (!reference.IsInverse) { targets.Add(reference.TargetId); } } return targets; } private sealed class ThrowingEnumerateProvider : IFileSystemProvider { public string MountName => "Throwing"; public bool IsWritable => false; public IOException EnumerationFailure { get; } = new("enumeration failed"); public int EnumerationCount { get; private set; } public ValueTask GetEntryAsync(string path, CancellationToken ct) { return new ValueTask((FileSystemEntry?)null); } public async IAsyncEnumerable EnumerateAsync( string path, [EnumeratorCancellation] CancellationToken ct) { EnumerationCount++; await Task.CompletedTask.ConfigureAwait(false); ct.ThrowIfCancellationRequested(); throw EnumerationFailure; #pragma warning disable CS0162 // unreachable: the iterator must still be an iterator yield break; #pragma warning restore CS0162 } public ValueTask OpenReadAsync(string path, CancellationToken ct) { throw new NotSupportedException(); } public ValueTask OpenWriteAsync(string path, FileWriteMode mode, CancellationToken ct) { throw new NotSupportedException(); } public ValueTask CreateDirectoryAsync(string path, CancellationToken ct) { throw new NotSupportedException(); } public ValueTask CreateFileAsync(string path, CancellationToken ct) { throw new NotSupportedException(); } public ValueTask DeleteAsync(string path, CancellationToken ct) { throw new NotSupportedException(); } public ValueTask MoveAsync(string source, string target, CancellationToken ct) { throw new NotSupportedException(); } public ValueTask CopyAsync(string source, string target, CancellationToken ct) { throw new NotSupportedException(); } } } }