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
1 change: 1 addition & 0 deletions NEXT_RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Redesigned item sharing so unrestricted links are supported and the created share URL is returned to the caller.
- Trimmed the CLI version string returned on Windows so `Version` no longer includes a trailing newline.
- Clarified in the docs that `GetItems(...)` returns summary items and `GetItem(...)` should be used before working with hydrated fields.
- Added `FileAttachments` support so item attachments can be created, listed, removed, and read through the wrapper.
- Added regression coverage for archive behavior, item sharing, version handling, and adding a new field to an existing built-in item.

## Migration
Expand Down
32 changes: 32 additions & 0 deletions OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ public void ArchiveItemStringOverloadUsesArchiveCommand()
Assert.That(fakeCli.LastArguments, Does.StartWith("item delete item-id --vault vault-id --archive"));
}

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

var reference = manager.GetFileAttachmentReference("file-id", "item-id", "vault-id");

Assert.That(reference, Is.EqualTo("op://vault-id/item-id/file-id?attr=content"));
}

[Test]
public void SaveFileAttachmentContentUsesGeneratedReference()
{
using var fakeCli = new FakeCli();
var manager = fakeCli.CreateManager();
var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

try
{
manager.SaveFileAttachmentContent("file-id", "item-id", "vault-id", outputPath);

Assert.That(fakeCli.LastArguments, Does.StartWith("read op://vault-id/item-id/file-id?attr=content --no-newline --force --out-file "));
Assert.That(fakeCli.LastArguments, Does.Contain(outputPath));
}
finally
{
if (File.Exists(outputPath))
File.Delete(outputPath);
}
}

[Test]
public void MoveItemStringOverloadUsesResolvedVaultIds()
{
Expand Down
71 changes: 70 additions & 1 deletion OnePassword.NET.Tests/TestItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class TestItems : TestsBase
{
private const string InitialTitle = "Created Item";
private const string InitialUsername = "Created Username";
private const string InitialAttachmentName = "Initial Attachment";
private const string InitialAttachmentContent = "Initial attachment content";
private const string EditSection = "Edit Section";
private const string EditField = "Edit Field";
private const string DeleteSection = "Delete Section";
Expand All @@ -23,8 +25,13 @@ public class TestItems : TestsBase
private const string FinalValue = "Test Value";
private const string AddedField = "Added Field";
private const string AddedValue = "Added Value";
private const string AddedAttachmentName = "Added Attachment";
private const string AddedAttachmentContent = "Added attachment content";
private const string Tag1 = "Tag1";
private const string Tag2 = "Tag2";
private readonly string _addedAttachmentFilePath = Path.Combine(WorkingDirectory, "AddedAttachment.txt");
private readonly string _addedAttachmentOutputFilePath = Path.Combine(WorkingDirectory, "AddedAttachment.out.txt");
private readonly string _initialAttachmentFilePath = Path.Combine(WorkingDirectory, "InitialAttachment.txt");

[Test]
[Order(1)]
Expand All @@ -40,6 +47,8 @@ public void CreateItem()
template.Fields.First(x => x.Label == "username").Value = InitialUsername;
template.Tags.Add(Tag1);
template.Tags.Add(Tag2);
File.WriteAllText(_initialAttachmentFilePath, InitialAttachmentContent);
template.FileAttachments.Add(new FileAttachment(_initialAttachmentFilePath, InitialAttachmentName));

var editSection = new Section(EditSection);
template.Sections.Add(editSection);
Expand All @@ -60,6 +69,9 @@ public void CreateItem()
Assert.That(_initialItem.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Type, Is.EqualTo(InitialType));
Assert.That(_initialItem.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(InitialValue));
Assert.That(_initialItem.Fields.First(x => x.Section?.Label == DeleteSection && x.Label == DeleteField).Value, Is.EqualTo(DeleteValue));
Assert.That(_initialItem.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.True);
Assert.That(_initialItem.FileAttachments.First(x => x.Name == InitialAttachmentName).ContentPath, Is.Not.Empty);
Assert.That(_initialItem.FileAttachments.First(x => x.Name == InitialAttachmentName).Size, Is.GreaterThan(0));
Assert.That(_initialItem.Tags, Does.Contain(Tag1));
Assert.That(_initialItem.Tags, Does.Contain(Tag2));
});
Expand Down Expand Up @@ -93,6 +105,7 @@ public void EditItem()
Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Type, Is.EqualTo(FinalType));
Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(FinalValue));
Assert.That(item.Fields.FirstOrDefault(x => x.Section?.Label == DeleteSection && x.Label == DeleteField), Is.Null);
Assert.That(item.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.True);
Assert.That(item.Tags, Does.Contain(Tag1));
Assert.That(item.Tags, Does.Not.Contain(Tag2));
});
Expand All @@ -119,6 +132,56 @@ public void EditItemAddsNewField()

[Test]
[Order(4)]
public void EditItemAddsFileAttachment()
{
if (!RunLiveTests)
Assert.Ignore();

Run(RunType.Test, () =>
{
File.WriteAllText(_addedAttachmentFilePath, AddedAttachmentContent);

var item = OnePassword.GetItem(_initialItem, TestVault);
item.FileAttachments.Add(new FileAttachment(_addedAttachmentFilePath, AddedAttachmentName));

var editedItem = OnePassword.EditItem(item, TestVault);

Assert.Multiple(() =>
{
Assert.That(editedItem.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.True);
Assert.That(editedItem.FileAttachments.Any(x => x.Name == AddedAttachmentName), Is.True);
});

_initialItem = editedItem;
});
}

[Test]
[Order(5)]
public void EditItemRemovesFileAttachment()
{
if (!RunLiveTests)
Assert.Ignore();

Run(RunType.Test, () =>
{
var item = OnePassword.GetItem(_initialItem, TestVault);
item.FileAttachments.Remove(item.FileAttachments.First(x => x.Name == InitialAttachmentName));

var editedItem = OnePassword.EditItem(item, TestVault);

Assert.Multiple(() =>
{
Assert.That(editedItem.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.False);
Assert.That(editedItem.FileAttachments.Any(x => x.Name == AddedAttachmentName), Is.True);
});

_initialItem = editedItem;
});
}

[Test]
[Order(6)]
public void GetItems()
{
if (!RunLiveTests)
Expand All @@ -143,7 +206,7 @@ public void GetItems()
}

[Test]
[Order(5)]
[Order(7)]
public void GetItem()
{
if (!RunLiveTests)
Expand All @@ -162,7 +225,13 @@ public void GetItem()
Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(FinalValue));
Assert.That(item.Fields.First(x => x.Label == AddedField).Value, Is.EqualTo(AddedValue));
Assert.That(item.Fields.FirstOrDefault(x => x.Section?.Label == DeleteSection && x.Label == DeleteField), Is.Null);
Assert.That(item.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.False);
Assert.That(item.FileAttachments.Any(x => x.Name == AddedAttachmentName), Is.True);
});

var attachment = item.FileAttachments.First(x => x.Name == AddedAttachmentName);
OnePassword.SaveFileAttachmentContent(attachment, item, TestVault, _addedAttachmentOutputFilePath);
Assert.That(File.ReadAllText(_addedAttachmentOutputFilePath), Is.EqualTo(AddedAttachmentContent));
});
}
}
34 changes: 34 additions & 0 deletions OnePassword.NET/IOnePasswordManager.Items.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ public ImmutableList<Item> SearchForItems(string? vaultId = null, bool? includeA
/// <exception cref="ArgumentException">Thrown when there is an invalid argument.</exception>
public Item EditItem(Item item, string vaultId);

/// <summary>Builds a secret reference for the specified file attachment's content.</summary>
/// <param name="fileAttachment">The file attachment.</param>
/// <param name="item">The item that contains the file attachment.</param>
/// <param name="vault">The vault that contains the item.</param>
/// <returns>The secret reference for the file attachment content.</returns>
/// <exception cref="ArgumentException">Thrown when there is an invalid argument.</exception>
public string GetFileAttachmentReference(FileAttachment fileAttachment, IItem item, IVault vault);

/// <summary>Builds a secret reference for the specified file attachment's content.</summary>
/// <param name="fileAttachmentId">The file attachment ID.</param>
/// <param name="itemId">The item ID.</param>
/// <param name="vaultId">The vault ID.</param>
/// <returns>The secret reference for the file attachment content.</returns>
/// <exception cref="ArgumentException">Thrown when there is an invalid argument.</exception>
public string GetFileAttachmentReference(string fileAttachmentId, string itemId, string vaultId);

/// <summary>Saves a file attachment's content to disk.</summary>
/// <param name="fileAttachment">The file attachment.</param>
/// <param name="item">The item that contains the file attachment.</param>
/// <param name="vault">The vault that contains the item.</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 SaveFileAttachmentContent(FileAttachment fileAttachment, IItem item, IVault vault, string filePath, string? fileMode = null);

/// <summary>Saves a file attachment's content to disk.</summary>
/// <param name="fileAttachmentId">The file attachment ID.</param>
/// <param name="itemId">The item ID.</param>
/// <param name="vaultId">The vault 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 SaveFileAttachmentContent(string fileAttachmentId, string itemId, string vaultId, string filePath, string? fileMode = null);

/// <summary>Archives an item.</summary>
/// <param name="item">The item to archive.</param>
/// <param name="vault">The vault that contains the item to archive.</param>
Expand Down
45 changes: 0 additions & 45 deletions OnePassword.NET/Items/File.cs

This file was deleted.

56 changes: 56 additions & 0 deletions OnePassword.NET/Items/FileAttachment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using OnePassword.Common;

namespace OnePassword.Items;

/// <summary>Represents a file attachment associated with an item.</summary>
public sealed class FileAttachment : ITracked
{
/// <summary>The section where the attachment is stored.</summary>
[JsonInclude]
[JsonPropertyName("section")]
public Section? Section { get; internal set; }

/// <summary>The attachment ID.</summary>
[JsonInclude]
[JsonPropertyName("id")]
public string Id { get; internal set; } = "";

/// <summary>The attachment name.</summary>
[JsonInclude]
[JsonPropertyName("name")]
public string Name { get; internal set; } = "";

/// <summary>The attachment size in bytes.</summary>
[JsonInclude]
[JsonPropertyName("size")]
public int Size { get; internal set; }

/// <summary>The path used to access the attachment content.</summary>
[JsonInclude]
[JsonPropertyName("content_path")]
public string ContentPath { get; internal set; } = "";

/// <inheritdoc />
public bool Changed => false;

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

/// <summary>Initializes a new instance of <see cref="FileAttachment" /> to attach a local file when creating or editing an item.</summary>
/// <param name="filePath">The local file path to attach.</param>
/// <param name="name">The attachment name. Leave empty to preserve the source file name.</param>
/// <param name="section">The section where the attachment should be added.</param>
public FileAttachment(string filePath, string? name = null, Section? section = null)
{
ContentPath = filePath ?? "";
Name = name ?? "";
Section = section;
}

/// <inheritdoc />
void ITracked.AcceptChanges()
{
}
}
8 changes: 5 additions & 3 deletions OnePassword.NET/Items/ItemBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ public string CategoryId
public TrackedList<string> Tags { get; internal set; } = [];

/// <summary>
/// The files associated with the item.
/// The file attachments associated with the item.
/// </summary>
[JsonInclude]
[JsonPropertyName("files")]
public TrackedList<File> Files { get; internal set; } = [];
public TrackedList<FileAttachment> FileAttachments { get; internal set; } = [];

/// <summary>
/// Returns <see langword="true" /> when the title has changed, <see langword="false" /> otherwise.
Expand All @@ -98,7 +98,8 @@ public string CategoryId
|| ((ITracked)Sections).Changed
|| ((ITracked)Fields).Changed
|| ((ITracked)Urls).Changed
|| ((ITracked)Tags).Changed;
|| ((ITracked)Tags).Changed
|| ((ITracked)FileAttachments).Changed;

/// <inheritdoc />
void ITracked.AcceptChanges()
Expand All @@ -109,5 +110,6 @@ void ITracked.AcceptChanges()
((ITracked)Fields).AcceptChanges();
((ITracked)Urls).AcceptChanges();
((ITracked)Tags).AcceptChanges();
((ITracked)FileAttachments).AcceptChanges();
}
}
Loading