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
21 changes: 16 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@ jobs:
validate:
name: Validate
runs-on: ubuntu-latest
outputs:
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
run_live_tests: ${{ steps.check-secrets.outputs.run_live_tests }}
steps:
- name: Check secrets
id: check-secrets
env:
OPT_ACCOUNT_ADDRESS: ${{ secrets.OPT_ACCOUNT_ADDRESS }}
OPT_ACCOUNT_EMAIL: ${{ secrets.OPT_ACCOUNT_EMAIL }}
OPT_ACCOUNT_NAME: ${{ secrets.OPT_ACCOUNT_NAME }}
OPT_ACCOUNT_PASSWORD: ${{ secrets.OPT_ACCOUNT_PASSWORD }}
OPT_ACCOUNT_SECRET_KEY: ${{ secrets.OPT_ACCOUNT_SECRET_KEY }}
OPT_CREATE_TEST_USER: ${{ secrets.OPT_CREATE_TEST_USER }}
OPT_RUN_LIVE_TESTS: ${{ secrets.OPT_RUN_LIVE_TESTS }}
OPT_TEST_USER_EMAIL: ${{ secrets.OPT_TEST_USER_EMAIL }}
run: |
if [[ -n "$OPT_ACCOUNT_ADDRESS" && -n "$OPT_ACCOUNT_EMAIL" && -n "$OPT_ACCOUNT_NAME" && -n "$OPT_ACCOUNT_PASSWORD" && -n "$OPT_ACCOUNT_SECRET_KEY" && -n "$OPT_CREATE_TEST_USER" && -n "$OPT_TEST_USER_EMAIL" ]]; then
echo "HAS_SECRETS=true" >> "$GITHUB_ENV"
echo "has_secrets=true" >> "$GITHUB_OUTPUT"
else
echo "HAS_SECRETS=false" >> "$GITHUB_ENV"
echo "has_secrets=false" >> "$GITHUB_OUTPUT"
fi

if [[ -n "$OPT_ACCOUNT_ADDRESS" && -n "$OPT_ACCOUNT_EMAIL" && -n "$OPT_ACCOUNT_NAME" && -n "$OPT_ACCOUNT_PASSWORD" && -n "$OPT_ACCOUNT_SECRET_KEY" && -n "$OPT_CREATE_TEST_USER" && -n "$OPT_TEST_USER_EMAIL" && "${OPT_RUN_LIVE_TESTS,,}" == "true" ]]; then
echo "run_live_tests=true" >> "$GITHUB_OUTPUT"
else
echo "run_live_tests=false" >> "$GITHUB_OUTPUT"
fi

build:
Expand All @@ -44,7 +55,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: 'csharp'
- name: Setup .NET 6.0
Expand All @@ -71,8 +82,8 @@ jobs:
OPT_ACCOUNT_PASSWORD: ${{ secrets.OPT_ACCOUNT_PASSWORD }}
OPT_ACCOUNT_SECRET_KEY: ${{ secrets.OPT_ACCOUNT_SECRET_KEY }}
OPT_CREATE_TEST_USER: ${{ secrets.OPT_CREATE_TEST_USER }}
OPT_RUN_LIVE_TESTS: ${{ env.HAS_SECRETS }}
OPT_RUN_LIVE_TESTS: ${{ needs.validate.outputs.run_live_tests }}
OPT_TEST_USER_EMAIL: ${{ secrets.OPT_TEST_USER_EMAIL }}
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
10 changes: 9 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
actions: read
contents: read
security-events: write
id-token: write

strategy:
fail-fast: false
Expand All @@ -37,5 +38,12 @@ jobs:
run: dotnet build --configuration Release --no-restore
- name: Pack
run: dotnet pack --configuration Release --no-build --output .

- name: NuGet Login
uses: NuGet/login@v1
id: nuget-login
with:
user: ${{secrets.NUGET_USER}}

- name: Push to NuGet
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
run: dotnet nuget push "*.nupkg" --api-key ${{steps.nuget-login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
21 changes: 21 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Guidance for AI contributors

This repository contains `OnePassword.NET`, a .NET wrapper for the 1Password CLI.

## Git Workflow

- When the user asks to commit changes, first check the current branch in the owning repository.
- If the current branch is not already a feature branch named in the format `feature/<appropriate-short-name>`, create one before committing.
- Choose a short, specific branch suffix that describes the work. Keep it lowercase and hyphenated.
- If already on an appropriate `feature/...` branch, do not create an additional branch unless the user asks for one.
- Commit messages must be a single-line short sentence in past tense that summarizes the commit.
- Commit messages must be written as a proper sentence and must end with a period.
- Do not use multiline commit messages, bullet lists, prefixes, or issue numbers in the commit message unless the user explicitly asks for them.
- After creating the commit, push the branch and its commits to `origin` unless the user explicitly says not to push.
- When the user says a pull request was merged, switch the owning repository back to `develop`, pull the latest changes from `origin`, prune deleted remote refs, and remove any local branches that no longer exist at `origin`.

## GitHub CLI

- Use the GitHub CLI (`gh`) for GitHub-related operations whenever possible.
- Prefer `gh` for pull request workflows, including creating, viewing, checking out, and reviewing pull requests.
- If `gh` is unavailable or unauthenticated, note that clearly before falling back to another approach.
2 changes: 1 addition & 1 deletion OnePassword.NET.Tests/AccountPreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public void GetAccount()
Assert.That(account.Id, Is.Not.Empty);
Assert.That(account.Name, Is.EqualTo(AccountName));
Assert.That(account.Domain, Is.EqualTo(AccountAddress[..AccountAddress.IndexOf('.', StringComparison.Ordinal)]));
Assert.That(account.Type, Is.EqualTo(AccountType.Business));
Assert.That(account.Type, Is.Not.EqualTo(AccountType.Unknown));
Assert.That(account.State, Is.EqualTo(State.Active));
Assert.That(account.Created, Is.Not.EqualTo(default));
});
Expand Down
55 changes: 46 additions & 9 deletions OnePassword.NET.Tests/Common/TestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ namespace OnePassword.Common;

public class TestsBase
{
private static readonly int CommandTimeout = int.Parse(GetEnv("OPT_COMMAND_TIMEOUT", "2"), CultureInfo.InvariantCulture) * 60 * 1000;
private protected static readonly int RateLimit = int.Parse(GetEnv("OPT_RATE_LIMIT", "250"), CultureInfo.InvariantCulture);
private protected static readonly bool RunLiveTests = bool.Parse(GetEnv("OPT_RUN_LIVE_TESTS", "false"));
private protected static readonly bool CreateTestUser = bool.Parse(GetEnv("OPT_CREATE_TEST_USER", "false"));
private static readonly int CommandTimeout = GetIntEnv("OPT_COMMAND_TIMEOUT", 2) * 60 * 1000;
private protected static readonly int RateLimit = GetIntEnv("OPT_RATE_LIMIT", 250);
private protected static readonly bool RunLiveTests = GetBoolEnv("OPT_RUN_LIVE_TESTS", false);
private protected static readonly bool CreateTestUser = GetBoolEnv("OPT_CREATE_TEST_USER", false);
private protected static readonly string AccountAddress = GetEnv("OPT_ACCOUNT_ADDRESS", "");
private protected static readonly string AccountEmail = GetEnv("OPT_ACCOUNT_EMAIL", "");
private protected static readonly string AccountName = GetEnv("OPT_ACCOUNT_NAME", "");
private protected static readonly string AccountPassword = GetEnv("OPT_ACCOUNT_PASSWORD", "");
private protected static readonly string AccountSecretKey = GetEnv("OPT_ACCOUNT_SECRET_KEY", "");
private protected static readonly string TestUserEmail = GetEnv("OPT_TEST_USER_EMAIL", "");
private static readonly string ServiceAccountToken = GetEnv("OPT_SERVICE_ACCOUNT_TOKEN", "");
private protected static readonly int TestUserConfirmTimeout = int.Parse(GetEnv("OPT_TEST_USER_CONFIRM_TIMEOUT", GetEnv("OPT_COMMAND_TIMEOUT", "2")), CultureInfo.InvariantCulture) * 60 * 1000;
private protected static readonly int TestUserConfirmTimeout = GetIntEnv("OPT_TEST_USER_CONFIRM_TIMEOUT", GetIntEnv("OPT_COMMAND_TIMEOUT", 2)) * 60 * 1000;
private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1);
private static readonly CancellationTokenSource TestCancellationTokenSource = new();
private static readonly CancellationTokenSource TearDownCancellationTokenSource = new();
Expand All @@ -42,6 +42,8 @@ public class TestsBase
private protected static IDocument TestDocument = null!;
private protected static Template TestTemplate = null!;
private protected static Item TestItem = null!;
private protected static bool GroupManagementSupported = true;
private protected static bool UserManagementSupported = true;
private protected static bool DoFinalTearDown;
private protected static readonly string WorkingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

Expand Down Expand Up @@ -99,6 +101,10 @@ protected static void Run(RunType runType, Action action)
{
action();
}
catch (NUnit.Framework.IgnoreException)
{
throw;
}
catch (Exception)
{
tokenSource.Cancel();
Expand All @@ -111,14 +117,45 @@ protected static void Run(RunType runType, Action action)
}
}

private static string GetEnv(string name, string value) =>
Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Machine)
?? Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process) ?? value;
private static bool GetBoolEnv(string name, bool value)
{
var environmentValue = GetEnv(name, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
return bool.TryParse(environmentValue, out var parsedValue) ? parsedValue : value;
}

private static int GetIntEnv(string name, int value)
{
var environmentValue = GetEnv(name, value.ToString(CultureInfo.InvariantCulture));
return int.TryParse(environmentValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue) ? parsedValue : value;
}

private static string GetEnv(string name, string value)
{
foreach (var target in new[]
{
EnvironmentVariableTarget.Machine,
EnvironmentVariableTarget.User,
EnvironmentVariableTarget.Process
})
{
var environmentValue = Environment.GetEnvironmentVariable(name, target);
if (!string.IsNullOrWhiteSpace(environmentValue))
return environmentValue;
}

return value;
}

private protected static void MarkManagementUnsupported()
{
GroupManagementSupported = false;
UserManagementSupported = false;
}

protected enum RunType
{
SetUp,
Test,
TearDown
}
}
}
18 changes: 17 additions & 1 deletion OnePassword.NET.Tests/SetUpGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@ public void CreateGroup()
{
if (!RunLiveTests)
Assert.Ignore();
if (!GroupManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
_initialGroup = OnePassword.CreateGroup(InitialName, InitialDescription);
try
{
_initialGroup = OnePassword.CreateGroup(InitialName, InitialDescription);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("(403) Forbidden", StringComparison.Ordinal))
{
MarkManagementUnsupported();
Assert.Ignore("Group management is not authorized for the current account.");
}

Assert.Multiple(() =>
{
Expand All @@ -44,6 +54,8 @@ public void EditGroup()
{
if (!RunLiveTests)
Assert.Ignore();
if (!GroupManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () => { OnePassword.EditGroup(_initialGroup, FinalName, FinalDescription); });
}
Expand All @@ -54,6 +66,8 @@ public void GetGroups()
{
if (!RunLiveTests)
Assert.Ignore();
if (!GroupManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
Expand All @@ -79,6 +93,8 @@ public void GetGroup()
{
if (!RunLiveTests)
Assert.Ignore();
if (!GroupManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
Expand Down
24 changes: 23 additions & 1 deletion OnePassword.NET.Tests/SetUpUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,20 @@ public void ProvisionUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
_initialUser = OnePassword.ProvisionUser(InitialName, TestUserEmail, Language.English);
try
{
_initialUser = OnePassword.ProvisionUser(InitialName, TestUserEmail, Language.English);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("(403) Forbidden", StringComparison.Ordinal))
{
MarkManagementUnsupported();
Assert.Ignore("User management is not authorized for the current account.");
}

Assert.Multiple(() =>
{
Expand All @@ -43,6 +53,8 @@ public void ConfirmUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
Expand Down Expand Up @@ -70,6 +82,8 @@ public void EditUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () => { OnePassword.EditUser(_initialUser, FinalName, false); });
}
Expand All @@ -80,6 +94,8 @@ public void GetUsers()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
Expand Down Expand Up @@ -108,6 +124,8 @@ public void GetUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () =>
{
Expand All @@ -133,6 +151,8 @@ public void SuspendUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () => { OnePassword.SuspendUser(TestUser, 1); });
}
Expand All @@ -143,6 +163,8 @@ public void ReactivateUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.SetUp, () => { OnePassword.ReactivateUser(TestUser); });
}
Expand Down
4 changes: 3 additions & 1 deletion OnePassword.NET.Tests/TearDownGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public void DeleteGroup()
{
if (!RunLiveTests)
Assert.Ignore();
if (!GroupManagementSupported)
Assert.Ignore();

Run(RunType.TearDown, () => { OnePassword.DeleteGroup(TestGroup); });
}
}
}
4 changes: 3 additions & 1 deletion OnePassword.NET.Tests/TearDownUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public void DeleteUser()
{
if (!RunLiveTests || !CreateTestUser)
Assert.Ignore();
if (!UserManagementSupported)
Assert.Ignore();

Run(RunType.TearDown, () => { OnePassword.DeleteUser(TestUser); });
}
}
}
6 changes: 6 additions & 0 deletions OnePassword.NET/Accounts/AccountType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public enum AccountType
[EnumMember(Value = "Family")]
Family,

/// <summary>
/// 1Password Individual Account
/// </summary>
[EnumMember(Value = "Individual")]
Individual,

/// <summary>
/// 1Password Personal Account
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion OnePassword.NET/Common/JsonStringEnumConverterEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe
if (_stringToEnum.TryGetValue(enumMemberString, out var enumValue))
return enumValue;

throw new NotImplementedException("Could not convert string value to its enum representation.");
if (_stringToEnum.TryGetValue("UNKNOWN", out var unknownValue))
return unknownValue;

throw new NotImplementedException($"Could not convert string value '{stringValue}' to its enum representation.");
}

/// <inheritdoc />
Expand Down
Loading
Loading