Skip to content

Commit ba794a1

Browse files
committed
Tests and FileCommand
1 parent 97ea139 commit ba794a1

27 files changed

+2086
-104
lines changed

.claude/settings.local.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(dotnet build:*)",
5+
"Bash(dotnet test:*)"
6+
],
7+
"deny": [],
8+
"ask": []
9+
}
10+
}

CLAUDE.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
NodeSwap is a Windows-only Node.js version manager written in C# (.NET 8.0). It's similar to NVM but specifically designed for Windows. The application uses symlinks to manage different Node.js versions and requires administrator privileges for symlink creation.
8+
9+
## Architecture
10+
11+
### Solution Structure
12+
- **NodeSwap** - Main console application (.NET 8.0-windows)
13+
- **NodeSwap.Tests** - MSTest unit tests using Shouldly assertions
14+
- **NodeSwap.Installer** - WiX installer project for Windows
15+
16+
### Core Components
17+
- **Program.cs** - Entry point with dependency injection setup using Microsoft.Extensions.DependencyInjection
18+
- **GlobalContext.cs** - Shared configuration and paths (storage, symlink, version tracking)
19+
- **Commands/** - CLI command implementations using DotMake.CommandLine with full dependency injection
20+
- **Interfaces/** - Abstraction interfaces for external dependencies (file system, process elevation, etc.)
21+
- **Services/** - Service implementations wrapping external dependencies
22+
- **NodeJs.cs** & **NodeJsWebApi.cs** - Node.js version management and API integration
23+
- **Utils/** - Helper utilities for console output, process elevation, and list operations
24+
25+
### Dependency Injection Architecture
26+
The application uses comprehensive dependency injection for improved testability:
27+
28+
#### Interfaces (NodeSwap/Interfaces/)
29+
- **IProcessElevation** - Windows process elevation and administrator checks
30+
- **IConsoleWriter** - Console output abstraction
31+
- **IFileSystem** - File system operations (read/write/delete/symlinks)
32+
- **INodeJsWebApi** - Node.js API integration for version lookup and downloads
33+
- **INodeJs** - Local Node.js version management
34+
35+
#### Services (NodeSwap/Services/)
36+
- **ProcessElevationService** - Windows-specific elevation implementation
37+
- **ConsoleWriterService** - Console.WriteLine wrapper
38+
- **FileSystemService** - System.IO wrapper with symlink support
39+
- **NodeJsWebApiService** - HTTP-based Node.js API client
40+
- **NodeJsService** - Local version management implementation
41+
42+
### Key Dependencies
43+
- **DotMake.CommandLine** - Primary CLI framework
44+
- **System.CommandLine** - Additional command line support
45+
- **NuGet.Versioning** - Version parsing and comparison
46+
- **ShellProgressBar** - Progress indication for downloads
47+
- **Microsoft.Extensions.DependencyInjection** - Dependency injection container
48+
49+
## Development Commands
50+
51+
### Building
52+
```bash
53+
dotnet build NodeSwap.sln
54+
dotnet build -c Release NodeSwap.sln
55+
```
56+
57+
### Testing
58+
```bash
59+
dotnet test NodeSwap.Tests/
60+
dotnet test --verbosity normal
61+
```
62+
63+
### Publishing
64+
```bash
65+
dotnet publish NodeSwap/NodeSwap.csproj -c Release
66+
```
67+
68+
### Running Locally
69+
```bash
70+
dotnet run --project NodeSwap/
71+
```
72+
73+
## Environment Requirements
74+
75+
### Prerequisites
76+
- .NET 8.0 runtime or SDK
77+
- Windows operating system (uses Windows-specific symlinks)
78+
- `NODESWAP_STORAGE` environment variable must be set to a valid directory path
79+
80+
### Admin Privileges
81+
The `use` command requires administrator privileges to create/update symlinks. The application will prompt for elevation when needed.
82+
83+
## Key Implementation Details
84+
85+
### Version Management
86+
- Node.js versions are downloaded and stored in `%NODESWAP_STORAGE%`
87+
- Active version tracked via symlink at `%NODESWAP_STORAGE%/current`
88+
- Version history maintained in `last-used` and `previous-used` files
89+
- Supports fuzzy version matching (e.g., "22" → "22.x.x")
90+
91+
### Command Structure
92+
All commands use dependency injection and inherit from DotMake.CommandLine patterns:
93+
- `list` - Show installed versions
94+
- `avail [min_version]` - Show available downloads
95+
- `install <version>` - Download and install Node.js version
96+
- `uninstall <version>` - Remove installed version
97+
- `use <version>` - Switch active version (requires admin)
98+
- `prev` - Switch to previously used version
99+
- `file` - Use or create a .nodeswap file to manage Node.js version for the current directory
100+
101+
#### Command Dependencies
102+
All commands now accept required dependencies through constructor injection:
103+
- **UseCommand** - Takes GlobalContext, INodeJs, IProcessElevation, IConsoleWriter, IFileSystem
104+
- **InstallCommand** - Takes GlobalContext, INodeJsWebApi, INodeJs, IConsoleWriter, IFileSystem
105+
- **PrevCommand** - Takes GlobalContext, INodeJs, IProcessElevation, IConsoleWriter, IFileSystem
106+
- **FileCommand** - Takes GlobalContext, INodeJs, IProcessElevation, IConsoleWriter, IFileSystem
107+
108+
### Testing Architecture
109+
110+
#### Test Framework
111+
- **MSTest** framework with **Shouldly** assertions
112+
- Comprehensive mock-based testing for isolation
113+
- 60+ unit tests covering all commands and utilities
114+
115+
#### Mock Services (NodeSwap.Tests/TestUtils/MockServices.cs)
116+
Custom mock implementations for complete test isolation:
117+
- **MockProcessElevation** - Configurable administrator status and elevation behavior
118+
- **MockConsoleWriter** - Captures output and error messages for verification
119+
- **MockFileSystem** - In-memory file system with symlink simulation
120+
- **MockNodeJs** - Configurable installed versions and active version tracking
121+
- **MockNodeJsWebApi** - Configurable API responses and exception testing
122+
123+
#### Test Structure
124+
- **Command Tests** - All commands have comprehensive test coverage using mocks
125+
- **Utility Tests** - Version parsing, list operations, and helper functions
126+
- **Integration-style Tests** - Some tests use MockServices with dependency injection
127+
- **Error Scenario Testing** - Exception handling, validation, and edge cases
128+
129+
#### Key Test Files
130+
- **FileCommandTests.cs** - Tests for .nodeswap file management (6 tests)
131+
- **UseCommandTests.cs** - Tests for version switching with mocks (7 tests)
132+
- **UseCommandTestsWithMocks.cs** - Additional comprehensive use command tests (10 tests)
133+
- **InstallCommandTestsWithMocks.cs** - Installation command tests (8 tests)
134+
- **PrevCommandTests.cs** - Previous version switching tests (6 tests)
135+
- **ListCommandTests.cs**, **UninstallCommandTests.cs**, **AvailCommandTests.cs** - Additional command coverage
136+
137+
#### Running Tests
138+
```bash
139+
# Run all tests
140+
dotnet test NodeSwap.Tests/ --verbosity normal
141+
142+
# Run specific test class
143+
dotnet test NodeSwap.Tests/ --filter "FileCommandTests" --verbosity normal
144+
145+
# Run tests before making changes to core logic
146+
dotnet test NodeSwap.Tests/
147+
```
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Threading.Tasks;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
using NodeSwap.Commands;
7+
using NodeSwap.Interfaces;
8+
using NodeSwap.Tests.TestUtils;
9+
using Shouldly;
10+
11+
namespace NodeSwap.Tests.Commands;
12+
13+
[TestClass]
14+
public class AvailCommandTests
15+
{
16+
private string _testDirectory;
17+
private GlobalContext _globalContext;
18+
private MockNodeJsWebApi _mockNodeJsWebApi;
19+
private MockConsoleWriter _mockConsoleWriter;
20+
21+
[TestInitialize]
22+
public void Setup()
23+
{
24+
_testDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
25+
Directory.CreateDirectory(_testDirectory);
26+
27+
_globalContext = new GlobalContext
28+
{
29+
StoragePath = Path.Combine(_testDirectory, "storage"),
30+
};
31+
Directory.CreateDirectory(_globalContext.StoragePath);
32+
33+
_mockNodeJsWebApi = new MockNodeJsWebApi();
34+
_mockConsoleWriter = new MockConsoleWriter();
35+
}
36+
37+
[TestCleanup]
38+
public void Cleanup()
39+
{
40+
if (Directory.Exists(_testDirectory))
41+
{
42+
Directory.Delete(_testDirectory, true);
43+
}
44+
}
45+
46+
[TestMethod]
47+
public async Task RunAsync_WhenNoPrefix_ShouldDisplayAllVersions()
48+
{
49+
var testVersions = new List<Version>
50+
{
51+
new(20, 11, 0),
52+
new(18, 17, 0),
53+
new(16, 14, 0),
54+
};
55+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = testVersions;
56+
57+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "" };
58+
var result = await availCommand.RunAsync();
59+
60+
result.ShouldBe(0);
61+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
62+
}
63+
64+
[TestMethod]
65+
public async Task RunAsync_WhenNullPrefix_ShouldDisplayAllVersions()
66+
{
67+
var testVersions = new List<Version>
68+
{
69+
new(20, 11, 0),
70+
new(18, 17, 0),
71+
new(16, 14, 0),
72+
};
73+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = testVersions;
74+
75+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = null };
76+
var result = await availCommand.RunAsync();
77+
78+
result.ShouldBe(0);
79+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
80+
}
81+
82+
[TestMethod]
83+
public async Task RunAsync_WhenSpecificPrefix_ShouldCallWithCorrectPrefix()
84+
{
85+
var filteredVersions = new List<Version> { new(18, 17, 0), new(18, 16, 0) };
86+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = filteredVersions;
87+
88+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "18" };
89+
var result = await availCommand.RunAsync();
90+
91+
result.ShouldBe(0);
92+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
93+
// Note: We can't easily test the prefix parameter without modifying MockNodeJsWebApi
94+
// but the command should pass it through correctly
95+
}
96+
97+
[TestMethod]
98+
public async Task RunAsync_WhenNoVersionsFound_ShouldReturnError()
99+
{
100+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = [];
101+
102+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "99" };
103+
var result = await availCommand.RunAsync();
104+
105+
result.ShouldBe(1);
106+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
107+
}
108+
109+
[TestMethod]
110+
public async Task RunAsync_WhenWebApiThrowsException_ShouldReturnError()
111+
{
112+
_mockNodeJsWebApi.ShouldThrowException = true;
113+
114+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "" };
115+
var result = await availCommand.RunAsync();
116+
117+
result.ShouldBe(1);
118+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
119+
}
120+
121+
[TestMethod]
122+
public async Task RunAsync_WhenVersionsAvailable_ShouldCallGetInstallableVersions()
123+
{
124+
var testVersions = new List<Version>
125+
{
126+
new(20, 11, 0),
127+
new(18, 17, 0),
128+
new(16, 14, 0),
129+
};
130+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = testVersions;
131+
132+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "" };
133+
var result = await availCommand.RunAsync();
134+
135+
result.ShouldBe(0);
136+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
137+
138+
testVersions.Count.ShouldBeGreaterThan(0);
139+
}
140+
141+
[TestMethod]
142+
public async Task RunAsync_WhenExactVersionPrefix_ShouldReturnSuccessfully()
143+
{
144+
var singleVersion = new List<Version> { new(18, 17, 0) };
145+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = singleVersion;
146+
147+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "18.17.0" };
148+
var result = await availCommand.RunAsync();
149+
150+
result.ShouldBe(0);
151+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
152+
}
153+
154+
[TestMethod]
155+
public async Task RunAsync_WhenEmptyStringPrefix_ShouldCallGetInstallableVersions()
156+
{
157+
var testVersions = new List<Version>
158+
{
159+
new(20, 11, 0),
160+
new(18, 17, 0),
161+
};
162+
_mockNodeJsWebApi.GetInstallableNodeVersionsReturn = testVersions;
163+
164+
var availCommand = new AvailCommand(_mockNodeJsWebApi, _mockConsoleWriter) { Prefix = "" };
165+
var result = await availCommand.RunAsync();
166+
167+
result.ShouldBe(0);
168+
_mockNodeJsWebApi.GetInstallableNodeVersionsCalled.ShouldBeTrue();
169+
}
170+
}

0 commit comments

Comments
 (0)