Skip to content
Open
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
83 changes: 82 additions & 1 deletion OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace OnePassword;
[TestFixture]
public class OnePasswordManagerCommandTests
{
private static readonly string[] EnvironmentVariableNames = ["API_URL", "CONNECTION", "EMPTY"];
private static readonly string[] EnvironmentVariableValues = ["https://example.com", "Server=db;Password=secret=", ""];
private static readonly string[] ParsedRecipients = ["one@example.com", "two@example.com"];

[Test]
Expand Down Expand Up @@ -166,6 +168,17 @@ public void GetSecretUsesTrimmedReference()
Assert.That(fakeCli.LastArguments, Does.StartWith("read op://vault/item/field --no-newline"));
}

[Test]
public void GetSecretPassesReferenceWithSpacesAsSingleArgument()
{
using var fakeCli = new FakeCli();
var manager = fakeCli.CreateManager();

manager.GetSecret("op://vault/item/field with spaces");

Assert.That(fakeCli.LastArgumentLines, Does.Contain("op://vault/item/field with spaces"));
}

[Test]
public void SaveSecretUsesTrimmedReference()
{
Expand All @@ -187,6 +200,46 @@ public void SaveSecretUsesTrimmedReference()
}
}

[Test]
public void GetEnvironmentVariablesUsesTrimmedEnvironmentIdAndParsesOutput()
{
using var fakeCli = new FakeCli(nextOutput: "API_URL=https://example.com\nCONNECTION=Server=db;Password=secret=\nEMPTY=\n");
var manager = fakeCli.CreateManager();

var variables = manager.GetEnvironmentVariables(" env-id ");

Assert.Multiple(() =>
{
Assert.That(fakeCli.LastArguments, Is.EqualTo("environment read env-id"));
Assert.That(variables.Select(x => x.Name), Is.EqualTo(EnvironmentVariableNames));
Assert.That(variables.Select(x => x.Value), Is.EqualTo(EnvironmentVariableValues));
});
}

[Test]
public void SaveEnvironmentVariablesUsesTrimmedEnvironmentIdAndWritesOutput()
{
using var fakeCli = new FakeCli(nextOutput: "API_URL=https://example.com\n");
var manager = fakeCli.CreateManager();
var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

try
{
manager.SaveEnvironmentVariables(" env-id ", outputPath);

Assert.Multiple(() =>
{
Assert.That(fakeCli.LastArguments, Is.EqualTo("environment read env-id"));
Assert.That(File.ReadAllText(outputPath), Is.EqualTo("API_URL=https://example.com\n"));
});
}
finally
{
if (File.Exists(outputPath))
File.Delete(outputPath);
}
}

[Test]
public void RevokeGroupPermissionsUsesVaultGroupCommand()
{
Expand Down Expand Up @@ -374,6 +427,19 @@ public void ShareItemWithMultipleEmailsUsesCommaSeparatedEmails()
Assert.That(fakeCli.LastArguments, Does.Contain("--emails one@example.com,two@example.com"));
}

[Test]
public void ShareItemPassesEmailValueWithSpacesAsSingleArgument()
{
using var fakeCli = new FakeCli();
var manager = fakeCli.CreateManager();

manager.ShareItem("item-id", "vault-id", ["recipient@example.com --view-once"]);

var emailFlagIndex = Array.IndexOf(fakeCli.LastArgumentLines, "--emails");
Assert.That(emailFlagIndex, Is.GreaterThanOrEqualTo(0));
Assert.That(fakeCli.LastArgumentLines[emailFlagIndex + 1], Is.EqualTo("recipient@example.com --view-once"));
}

[Test]
public void ShareItemWithEmptyEmailCollectionOmitsEmailsFlag()
{
Expand Down Expand Up @@ -439,6 +505,7 @@ public void ShareItemParsesStructuredShareResult()
private sealed class FakeCli : IDisposable
{
private readonly string _argumentsPath;
private readonly string _argumentLinesPath;
private readonly string _directoryPath;
private readonly string _errorOutputPath;
private readonly string _inputPath;
Expand All @@ -452,6 +519,7 @@ public FakeCli(string versionOutput = "2.32.1\n", string nextOutput = "{}", stri
{
_directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
_argumentsPath = Path.Combine(_directoryPath, "last-arguments.txt");
_argumentLinesPath = Path.Combine(_directoryPath, "last-argument-lines.txt");
_errorOutputPath = Path.Combine(_directoryPath, "error-output.txt");
_inputPath = Path.Combine(_directoryPath, "last-input.txt");
_nextOutputPath = Path.Combine(_directoryPath, "next-output.txt");
Expand Down Expand Up @@ -485,7 +553,9 @@ public FakeCli(string versionOutput = "2.32.1\n", string nextOutput = "{}", stri

public string ExecutableName { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "op.cmd" : "op";

public string LastArguments => File.Exists(_argumentsPath) ? File.ReadAllText(_argumentsPath) : "";
public string LastArguments => File.Exists(_argumentsPath) ? File.ReadAllText(_argumentsPath).TrimEnd('\r', '\n') : "";

public string[] LastArgumentLines => File.Exists(_argumentLinesPath) ? File.ReadAllLines(_argumentLinesPath) : [];

public string LastInput => File.Exists(_inputPath) ? File.ReadAllText(_inputPath) : "";

Expand Down Expand Up @@ -529,6 +599,16 @@ private static string GetScript(string versionOutputFileName)
@echo off
setlocal
> "%~dp0last-arguments.txt" echo %*
break > "%~dp0last-argument-lines.txt"
if not "%~1"=="" >> "%~dp0last-argument-lines.txt" echo(%~1
if not "%~2"=="" >> "%~dp0last-argument-lines.txt" echo(%~2
if not "%~3"=="" >> "%~dp0last-argument-lines.txt" echo(%~3
if not "%~4"=="" >> "%~dp0last-argument-lines.txt" echo(%~4
if not "%~5"=="" >> "%~dp0last-argument-lines.txt" echo(%~5
if not "%~6"=="" >> "%~dp0last-argument-lines.txt" echo(%~6
if not "%~7"=="" >> "%~dp0last-argument-lines.txt" echo(%~7
if not "%~8"=="" >> "%~dp0last-argument-lines.txt" echo(%~8
if not "%~9"=="" >> "%~dp0last-argument-lines.txt" echo(%~9
> "%~dp0last-input.txt" more
if "%~1"=="update" (
if exist "%~dp0update-payload.zip" copy /y "%~dp0update-payload.zip" "%~3\update-payload.zip" > nul
Expand All @@ -549,6 +629,7 @@ @echo off
#!/bin/sh
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
printf '%s' "$*" > "$script_dir/last-arguments.txt"
printf '%s\n' "$@" > "$script_dir/last-argument-lines.txt"
cat > "$script_dir/last-input.txt"
if [ "$1" = "update" ]; then
if [ -f "$script_dir/update-payload.zip" ]; then
Expand Down
28 changes: 28 additions & 0 deletions OnePassword.NET/Environments/EnvironmentVariable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace OnePassword.Environments;

/// <summary>Represents a variable from a 1Password Environment.</summary>
public sealed class EnvironmentVariable
{
/// <summary>The variable name.</summary>
public string Name { get; internal set; } = "";

/// <summary>The variable value.</summary>
public string Value { get; internal set; } = "";

/// <summary>Initializes a new instance of <see cref="EnvironmentVariable" />.</summary>
public EnvironmentVariable()
{
}

/// <summary>Initializes a new instance of <see cref="EnvironmentVariable" /> with the specified name and value.</summary>
/// <param name="name">The variable name.</param>
/// <param name="value">The variable value.</param>
public EnvironmentVariable(string name, string value)
{
Name = name ?? "";
Value = value ?? "";
}

/// <inheritdoc />
public override string ToString() => $"{Name}={Value}";
}
19 changes: 19 additions & 0 deletions OnePassword.NET/IOnePasswordManager.Environments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using OnePassword.Environments;

namespace OnePassword;

public partial interface IOnePasswordManager
{
/// <summary>Gets the environment variables for a 1Password Environment.</summary>
/// <param name="environmentId">The Environment ID.</param>
/// <returns>The environment variables.</returns>
/// <exception cref="ArgumentException">Thrown when there is an invalid argument.</exception>
public ImmutableList<EnvironmentVariable> GetEnvironmentVariables(string environmentId);

/// <summary>Saves the environment variables for a 1Password Environment to disk.</summary>
/// <param name="environmentId">The Environment ID.</param>
/// <param name="filePath">The output file path.</param>
/// <param name="fileMode">The file mode to use when creating the file.</param>
/// <exception cref="ArgumentException">Thrown when there is an invalid argument.</exception>
public void SaveEnvironmentVariables(string environmentId, string filePath, string? fileMode = null);
}
5 changes: 4 additions & 1 deletion OnePassword.NET/OnePassword.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Company>Jean-Sebastien Carle</Company>
<Product>OnePassword.NET</Product>
<Description>1Password CLI Wrapper</Description>
<Version>2.5.0</Version>
<Version>2.5.0-daydream.1</Version>
<AssemblyVersion>2.5.0.0</AssemblyVersion>
<FileVersion>2.5.0.0</FileVersion>
<Copyright>Copyright © Jean-Sebastien Carle 2021-2025</Copyright>
Expand Down Expand Up @@ -65,6 +65,9 @@
<Compile Update="IOnePasswordManager.Documents.cs">
<DependentUpon>OnePasswordManager.Documents.cs</DependentUpon>
</Compile>
<Compile Update="IOnePasswordManager.Environments.cs">
<DependentUpon>OnePasswordManager.Environments.cs</DependentUpon>
</Compile>
<Compile Update="IOnePasswordManager.Groups.cs">
<DependentUpon>OnePasswordManager.Groups.cs</DependentUpon>
</Compile>
Expand Down
22 changes: 12 additions & 10 deletions OnePassword.NET/OnePasswordManager.Accounts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public ImmutableList<Account> GetAccounts()
if (_mode == Mode.ServiceAccount)
throw new InvalidOperationException($"{nameof(GetAccounts)} is not supported when using service accounts.");

const string command = "account list";
var command = new OpCommand("account", "list");
return Op(JsonContext.Default.ImmutableListAccount, command);
}

Expand All @@ -32,7 +32,9 @@ public AccountDetails GetAccount(string account = "")

var trimmedAccount = account?.Trim() ?? "";

var command = trimmedAccount.Length > 0 ? $"account get --account \"{trimmedAccount}\"" : "account get";
var command = new OpCommand("account", "get");
if (trimmedAccount.Length > 0)
command.Add("--account", trimmedAccount);
return Op(JsonContext.Default.AccountDetails, command);
}

Expand Down Expand Up @@ -68,9 +70,9 @@ public void AddAccount(string address, string email, string secretKey, string pa

var trimmedShorthand = shorthand?.Trim() ?? "";

var command = $"account add --address \"{trimmedAddress}\" --email \"{trimmedEmail}\" --secret-key \"{trimmedSecretKey}\"";
var command = new OpCommand("account", "add", "--address", trimmedAddress, "--email", trimmedEmail, "--secret-key", trimmedSecretKey);
if (trimmedShorthand.Length > 0)
command += $" --shorthand \"{trimmedShorthand}\"";
command.Add("--shorthand", trimmedShorthand);

var result = Op(command, trimmedPassword, true);
if (result.Contains("No saved device ID.", StringComparison.Ordinal))
Expand Down Expand Up @@ -115,7 +117,7 @@ public void SignIn(string? password = null)
throw new ArgumentException($"{nameof(password)} cannot be empty.", nameof(password));
}

const string command = "signin --force --raw";
var command = new OpCommand("signin", "--force", "--raw");
var result = Op(command, password?.Trim());
_session = result.Trim();
}
Expand All @@ -126,11 +128,11 @@ public void SignOut(bool all = false)
if (_mode == Mode.ServiceAccount)
throw new InvalidOperationException($"{nameof(SignOut)} is not supported when using service accounts.");

var command = "signout";
var command = new OpCommand("signout");
if (all)
command += " --all";
command.Add("--all");
else if (_account.Length > 0)
command += $" --account \"{_account}\"";
command.Add("--account", _account);
Op(command);
_session = "";
}
Expand All @@ -146,8 +148,8 @@ public ImmutableList<string> ForgetAccount(bool all = false)
if (_session.Length > 0)
SignOut(all);

var command = "account forget";
command += all ? " --all" : $" \"{_account}\"";
var command = new OpCommand("account", "forget");
command.Add(all ? "--all" : _account);

var result = Op(command);

Expand Down
40 changes: 20 additions & 20 deletions OnePassword.NET/OnePasswordManager.Documents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public ImmutableList<DocumentDetails> GetDocuments(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));

var command = $"document list --vault {vaultId}";
var command = new OpCommand("document", "list", "--vault", vaultId);
return Op(JsonContext.Default.ImmutableListDocumentDetails, command);
}

Expand All @@ -40,11 +40,11 @@ public ImmutableList<DocumentDetails> SearchForDocuments(string? vaultId = null,
if (vaultId is not null && vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));

var command = "document list";
var command = new OpCommand("document", "list");
if (vaultId is not null)
command += $" --vault {vaultId}";
command.Add("--vault", vaultId);
if (includeArchive is not null && includeArchive.Value)
command += " --include-archive";
command.Add("--include-archive");
return Op(JsonContext.Default.ImmutableListDocumentDetails, command);
}

Expand Down Expand Up @@ -80,9 +80,9 @@ public void GetDocument(string documentId, string vaultId, string filePath, stri
var trimmedFileMode = fileMode?.Trim();

// Not specifying --force will hang waiting for user input if the file exists.
var command = $"document get {documentId} --out-file \"{trimmedFilePath}\" --force --vault {vaultId}";
var command = new OpCommand("document", "get", documentId, "--out-file", trimmedFilePath, "--force", "--vault", vaultId);
if (trimmedFileMode is not null)
command += $" --file-mode {trimmedFileMode}";
command.Add("--file-mode", trimmedFileMode);
Op(command);
}

Expand Down Expand Up @@ -119,13 +119,13 @@ public void SearchForDocument(string documentId, string filePath, string? vaultI
var trimmedFileMode = fileMode?.Trim();

// Not specifying --force will hang waiting for user input if the file exists.
var command = $"document get {documentId} --out-file \"{trimmedFilePath}\" --force";
var command = new OpCommand("document", "get", documentId, "--out-file", trimmedFilePath, "--force");
if (vaultId is not null)
command += $" --vault {vaultId}";
command.Add("--vault", vaultId);
if (includeArchive is not null && includeArchive.Value)
command += " --include-archive";
command.Add("--include-archive");
if (trimmedFileMode is not null)
command += $" --file-mode {trimmedFileMode}";
command.Add("--file-mode", trimmedFileMode);
Op(command);
}

Expand Down Expand Up @@ -177,13 +177,13 @@ public Document CreateDocument(string vaultId, string filePath, string? fileName
var trimmedFileName = fileName?.Trim();
var trimmedTitle = title?.Trim();

var command = $"document create \"{trimmedFilePath}\" --vault {vaultId}";
var command = new OpCommand("document", "create", trimmedFilePath, "--vault", vaultId);
if (trimmedFileName is not null)
command += $" --file-name \"{trimmedFileName}\"";
command.Add("--file-name", trimmedFileName);
if (trimmedTitle is not null)
command += $" --title \"{trimmedTitle}\"";
command.Add("--title", trimmedTitle);
if (tags is not null && tags.Count > 0)
command += $" --tags \"{tags.ToCommaSeparated()}\"";
command.Add("--tags", tags.ToCommaSeparated());
return Op(JsonContext.Default.Document, command);
}

Expand Down Expand Up @@ -223,13 +223,13 @@ public void ReplaceDocument(string documentId, string vaultId, string filePath,
var trimmedFileName = fileName?.Trim();
var trimmedTitle = title?.Trim();

var command = $"document edit {documentId} \"{trimmedFilePath}\" --vault {vaultId}";
var command = new OpCommand("document", "edit", documentId, trimmedFilePath, "--vault", vaultId);
if (trimmedFileName is not null)
command += $" --file-name \"{trimmedFileName}\"";
command.Add("--file-name", trimmedFileName);
if (trimmedTitle is not null)
command += $" --title \"{trimmedTitle}\"";
command.Add("--title", trimmedTitle);
if (tags is not null && tags.Count > 0)
command += $" --tags \"{tags.ToCommaSeparated()}\"";
command.Add("--tags", tags.ToCommaSeparated());
Op(command);
}

Expand All @@ -252,7 +252,7 @@ public void ArchiveDocument(string documentId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));

var command = $"document delete {documentId} --vault {vaultId} --archive";
var command = new OpCommand("document", "delete", documentId, "--vault", vaultId, "--archive");
Op(command);
}

Expand All @@ -275,7 +275,7 @@ public void DeleteDocument(string documentId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));

var command = $"document delete {documentId} --vault {vaultId}";
var command = new OpCommand("document", "delete", documentId, "--vault", vaultId);
Op(command);
}
}
Loading