Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### New features

- `sproc` command to manage Cosmos DB for NoSQL stored procedures on the current container: `list`, `show`, `exists` (returns a boolean usable in `if`/`while` conditions), `create` (from a JavaScript file or piped body, with `--force` to replace), `exec` (with a JSON argument array and `--partition-key`), `edit` (interactive external editor), and `delete`. ([#103](https://github.com/Azure/CosmosDBShell/issues/103))

## 1.1.4-preview — 2026-05-21

First release on the 1.1 line. A pretty packed cycle. The headline change is **ARM-based control plane for database and container management**, but there’s also a fully reworked CLI, two new item commands, a much friendlier shell experience for newcomers, and a long list of paper-cut fixes.
Expand Down
116 changes: 116 additions & 0 deletions CosmosDBShell.Tests/CommandTests/SprocCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace CosmosShell.Tests.CommandTests;

using System.Text.Json;
using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Microsoft.Azure.Cosmos;

/// <summary>
/// Unit tests for the pure helpers on <see cref="SprocCommand"/>: subcommand
/// normalization, execution-parameter parsing, and partition-key parsing.
/// </summary>
public class SprocCommandTests
{
[Theory]
[InlineData("LIST", "list")]
[InlineData(" Show ", "show")]
[InlineData("Create", "create")]
[InlineData(null, "")]
[InlineData("", "")]
public void NormalizeSubcommand_TrimsAndLowercases(string? input, string expected)
{
Assert.Equal(expected, SprocCommand.NormalizeSubcommand(input));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ParseExecParams_NullOrWhitespace_ReturnsEmpty(string? input)
{
Assert.Empty(SprocCommand.ParseExecParams(input));
}

[Fact]
public void ParseExecParams_JsonArray_ReturnsElements()
{
var parameters = SprocCommand.ParseExecParams("[\"a\", 1, true]");

Assert.Equal(3, parameters.Length);
Assert.Equal("a", ((JsonElement)parameters[0]).GetString());
Assert.Equal(1, ((JsonElement)parameters[1]).GetInt32());
Assert.True(((JsonElement)parameters[2]).GetBoolean());
}

[Fact]
public void ParseExecParams_EmptyArray_ReturnsEmpty()
{
Assert.Empty(SprocCommand.ParseExecParams("[]"));
}

[Theory]
[InlineData("not json")]
[InlineData("{\"a\":1}")]
[InlineData("\"justAString\"")]
[InlineData("42")]
public void ParseExecParams_NonArray_Throws(string input)
{
Assert.Throws<CommandException>(() => SprocCommand.ParseExecParams(input));
}

[Fact]
public void ParsePartitionKey_String_MatchesStringPartitionKey()
{
Assert.Equal(new PartitionKey("pk1").ToString(), SprocCommand.ParsePartitionKey("pk1").ToString());
}

[Fact]
public void ParsePartitionKey_QuotedString_PreservesStringType()
{
Assert.Equal(new PartitionKey("pk1").ToString(), SprocCommand.ParsePartitionKey("\"pk1\"").ToString());
}

[Fact]
public void ParsePartitionKey_Number_PreservesNumericType()
{
Assert.Equal(new PartitionKey(42).ToString(), SprocCommand.ParsePartitionKey("42").ToString());
}

[Fact]
public void ParsePartitionKey_Boolean_PreservesBooleanType()
{
Assert.Equal(new PartitionKey(true).ToString(), SprocCommand.ParsePartitionKey("true").ToString());
}

[Fact]
public void ParsePartitionKey_JsonArray_BuildsHierarchicalPartitionKey()
{
var expected = new PartitionKeyBuilder()
.Add("tenant")
.Add("user")
.Build();

Assert.Equal(expected.ToString(), SprocCommand.ParsePartitionKey("[\"tenant\",\"user\"]").ToString());
}

[Fact]
public void ParsePartitionKey_JsonObject_Throws()
{
Assert.Throws<CommandException>(() => SprocCommand.ParsePartitionKey("{\"a\":1}"));
}

[Fact]
public void DefaultStoredProcedureBody_IsValidSeedTemplate()
{
var body = SprocCommand.DefaultStoredProcedureBody();

Assert.False(string.IsNullOrWhiteSpace(body));
Assert.Contains("function sample", body);
Assert.Contains("getContext().getCollection()", body);
Assert.Contains("queryDocuments", body);
}
}
37 changes: 36 additions & 1 deletion CosmosDBShell.Tests/Integration/ControlFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,39 @@ public async Task DoWhile_ExecutesAtLeastOnce()
var n = Assert.IsType<ShellNumber>(i);
Assert.Equal(1, n.Value);
}
}

[Fact]
public async Task IfElse_CommandExpressionCondition_TrueBranch()
{
var state = await RunScriptAsync("if (echo \"true\") { $x = 1 } else { $x = 2 }");

Assert.False(state.IsError, FormatError(state));
var x = GetVariable("x");
var n = Assert.IsType<ShellNumber>(x);
Assert.Equal(1, n.Value);
}

[Fact]
public async Task IfElse_CommandExpressionCondition_FalseBranch()
{
var state = await RunScriptAsync("if (echo \"false\") { $x = 1 } else { $x = 2 }");

Assert.False(state.IsError, FormatError(state));
var x = GetVariable("x");
var n = Assert.IsType<ShellNumber>(x);
Assert.Equal(2, n.Value);
}

[Fact]
public async Task CommandStatement_DoesNotInheritIsPrintedFromPriorCommand()
{
// A prior command that already printed its own output sets IsPrinted on the shared
// state. The following command's output must still be printed and not suppressed.
var outputFile = CaptureOutputFile();
var state = await RunScriptAsync("{ version\necho \"AFTER\" }");

Assert.False(state.IsError, FormatError(state));
var output = await ReadRedirectAsync(outputFile);
Assert.Contains("AFTER", output, StringComparison.Ordinal);
}
}
15 changes: 15 additions & 0 deletions CosmosDBShell.Tests/ToolOperationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ public void GetTool_AppendsUserOnlyWarningForRestrictedCommands()
Assert.Contains("cannot be invoked through MCP", tool.Description, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void GetTool_MarksStoredProceduresRestrictedForMcp()
{
var factory = new CommandRunner().Commands["sproc"];

Assert.True(factory.McpRestricted);

var tool = ToolOperations.GetTool(factory);

Assert.Contains("cannot be invoked through MCP", tool.Description, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(tool.Annotations);
Assert.True(tool.Annotations!.DestructiveHint);
Assert.True(tool.Annotations.OpenWorldHint);
}

[Fact]
public void GetTool_DoesNotAppendWarningForUnrestrictedCommands()
{
Expand Down
Loading
Loading