Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/DynamoCore/Configuration/PathManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,13 @@ private void BuildCommonDirectories()
commonDataDir = GetCommonDataFolder();

defaultTemplatesDirectory = GetTemplateFolder(commonDataDir);

// Seed the active templates directory with the default so it is never null.
// A preferred location supplied later through UpdatePreferenceItemPath may be
// rejected (unwritable %ProgramData%, unavailable network share, invalid path),
// and callers must still get a usable path rather than null. See DYN-10661.
templatesDirectory = defaultTemplatesDirectory;

rootDirectories = new List<string> { userDataDir };

nodeDirectories = new HashSet<string>
Expand Down
9 changes: 8 additions & 1 deletion src/DynamoCore/Models/DynamoModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1909,7 +1909,14 @@ private void InitializePreferenceLocations()


UpdatePreferenceItemLocation(PreferenceItem.Backup, PreferenceSettings.BackupLocation);
UpdatePreferenceItemLocation(PreferenceItem.Templates, PreferenceSettings.TemplateFilePath);
if (!UpdatePreferenceItemLocation(PreferenceItem.Templates, PreferenceSettings.TemplateFilePath))
{
// The preferred templates location could not be used, so the default stays
// in effect. This previously failed silently and only surfaced downstream as
// a null templates directory. See DYN-10661.
Logger?.Log("Could not use templates location '" + PreferenceSettings.TemplateFilePath +
"'. Falling back to '" + pathManager.TemplatesDirectory + "'.", LogLevel.File);
}
}
internal bool UpdatePreferenceItemLocation(PreferenceItem item, string newLocation)
{
Expand Down
61 changes: 61 additions & 0 deletions test/DynamoCoreTests/Configuration/PathManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.IO;
using Dynamo.Core;
using NUnit.Framework;

namespace Dynamo.Tests.Configuration
{
[TestFixture]
class PathManagerTests : UnitTestBase
{
private static PathManager MakePathManager()
{
return new PathManager(new PathManagerParams
{
CorePath = Path.GetDirectoryName(typeof(PathManager).Assembly.Location)
});
}

/// <summary>
/// DYN-10661: TemplatesDirectory has no default value. It is only ever assigned by
/// UpdatePreferenceItemPath, so a PathManager that has not had that call succeed
/// reports a null templates directory even though DefaultTemplatesDirectory is valid.
/// </summary>
[Test]
[Category("UnitTests")]
public void WhenPathManagerIsConstructedThenTemplatesDirectoryIsNotNull()
{
var pathManager = MakePathManager();

Assert.That(pathManager.DefaultTemplatesDirectory, Is.Not.Null.And.Not.Empty,
"DefaultTemplatesDirectory is built by the constructor and should always be set.");
Assert.That(pathManager.TemplatesDirectory, Is.Not.Null.And.Not.Empty,
"TemplatesDirectory must never be null - callers use it in string and path operations.");
}

/// <summary>
/// DYN-10661: when the preferred templates location cannot be created (locked-down
/// %ProgramData%, unavailable network share, invalid path), UpdatePreferenceItemPath
/// returns false without assigning templatesDirectory, permanently leaving the
/// property null for the rest of the session. It should fall back to the default.
/// </summary>
[Test]
[Category("UnitTests")]
public void WhenTemplateLocationCannotBeCreatedThenTemplatesDirectoryFallsBackToDefault()
{
var pathManager = MakePathManager();

// A file occupying the target path makes Directory.CreateDirectory throw
// IOException, which is how PathHelper.CreateFolderIfNotExist reports an
// unusable location without surfacing an error to the user.
var blockedTemplateLocation = Path.Combine(TempFolder, "blockedTemplates");
File.WriteAllText(blockedTemplateLocation, string.Empty);

var updated = pathManager.UpdatePreferenceItemPath(
PathManager.PreferenceItem.Templates, blockedTemplateLocation);

Assert.That(updated, Is.False, "An unusable location should be rejected.");
Assert.That(pathManager.TemplatesDirectory, Is.EqualTo(pathManager.DefaultTemplatesDirectory),
"A rejected templates location must leave the default in effect, not null.");
}
}
}
15 changes: 15 additions & 0 deletions test/DynamoCoreWpf3Tests/WorkspaceSaving.cs
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,21 @@ public void TemplateSavePathCheckBlocksTemplateRootAndChildren()
Path.Combine(templateRoot, "fr-FR", "Template.dyn"), localizedTemplateDirectory));
}

/// <summary>
/// DYN-10661: Save/SaveAs threw ArgumentNullException when the templates directory
/// was null, because the check was a bare path.Contains(templatesDirectory).
/// The check must treat an unknown templates directory as "not a template path".
/// </summary>
[Test]
[Category("UnitTests")]
public void TemplateSavePathCheckDoesNotThrowWhenTemplateDirectoryIsUnknown()
{
var savePath = Path.Combine(TempFolder, "Workspace.dyn");

Assert.IsFalse(DynamoViewModel.IsPathInTemplateDirectoryTree(savePath, null));
Assert.IsFalse(DynamoViewModel.IsPathInTemplateDirectoryTree(savePath, string.Empty));
}

[Test]
[Category("UnitTests")]
public void TemplateSavePathCheckAllowsPathsOutsideTemplateRoot()
Expand Down
Loading