Skip to content

Commit

Permalink
Feature create user (#265)
Browse files Browse the repository at this point in the history
* Add test fixtures for existing UserApiClient method.

#261

* Implement creating a user.

#261
  • Loading branch information
DiscoPYF authored Jul 13, 2020
1 parent 3bae94b commit f1fbee5
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 3 deletions.
4 changes: 2 additions & 2 deletions arangodb-net-standard.Test/ApiClientTestFixtureBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ namespace ArangoDBNetStandardTest
{
public abstract class ApiClientTestFixtureBase : IDisposable, IAsyncLifetime
{
private List<string> _databases = new List<string>();
private List<string> _users = new List<string>();
protected readonly List<string> _databases = new List<string>();
protected readonly List<string> _users = new List<string>();

private readonly List<HttpApiTransport> _transports = new List<HttpApiTransport>();

Expand Down
88 changes: 88 additions & 0 deletions arangodb-net-standard.Test/UserApi/UserApiClientTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using ArangoDBNetStandard;
using ArangoDBNetStandard.UserApi;
using ArangoDBNetStandard.UserApi.Models;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Xunit;

namespace ArangoDBNetStandardTest.UserApi
{
public class UserApiClientTest : IClassFixture<UserApiClientTestFixture>
{
private readonly IUserApiClient _userClient;

private readonly UserApiClientTestFixture _fixture;

public UserApiClientTest(UserApiClientTestFixture fixture)
{
_userClient = fixture.ArangoClient.User;
_fixture = fixture;
}

[Fact]
public async Task DeleteUserAsync_ShouldSucceed()
{
var deleteResponse = await _userClient.DeleteUserAsync(
_fixture.UsernameToDelete);

Assert.False(deleteResponse.Error);
Assert.Equal(HttpStatusCode.Accepted, deleteResponse.Code);
}

[Fact]
public async Task DeleteUserAsync_ShouldThrow_WhenUserDoesNotExist()
{
var ex = await Assert.ThrowsAsync<ApiErrorException>(async () =>
await _userClient.DeleteUserAsync(
nameof(DeleteUserAsync_ShouldThrow_WhenUserDoesNotExist)));

Assert.True(ex.ApiError.Error);
Assert.Equal(HttpStatusCode.NotFound, ex.ApiError.Code);
Assert.NotNull(ex.ApiError.ErrorMessage);
Assert.Equal(1703, ex.ApiError.ErrorNum); // ERROR_USER_NOT_FOUND
}

[Fact]
public async Task PostUserAsync_ShouldSucceed()
{
PostUserResponse postResponse = await _userClient.PostUserAsync(
new PostUserBody()
{
User = _fixture.UsernameToCreate,
Passwd = "password",
Extra = new Dictionary<string, object>()
{
["somedata"] = "here"
}
});

Assert.False(postResponse.Error);
Assert.Equal(HttpStatusCode.Created, postResponse.Code);
Assert.Equal(_fixture.UsernameToCreate, postResponse.User);
Assert.True(postResponse.Active);
Assert.True(postResponse.Extra.ContainsKey("somedata"));
Assert.Equal("here", postResponse.Extra["somedata"].ToString());
}

[Fact]
public async Task PostUserAsync_ShouldThrow_WhenUserAlreadyExist()
{
var ex = await Assert.ThrowsAsync<ApiErrorException>(async () =>
await _userClient.PostUserAsync(new PostUserBody()
{
User = _fixture.UsernameExisting,
Passwd = "password",
Extra = new Dictionary<string, object>()
{
["somedata"] = "here"
}
}));

Assert.True(ex.ApiError.Error);
Assert.Equal(HttpStatusCode.Conflict, ex.ApiError.Code);
Assert.NotNull(ex.ApiError.ErrorMessage);
Assert.Equal(1702, ex.ApiError.ErrorNum); // ERROR_USER_DUPLICATE
}
}
}
50 changes: 50 additions & 0 deletions arangodb-net-standard.Test/UserApi/UserApiClientTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using ArangoDBNetStandard;
using ArangoDBNetStandard.DatabaseApi.Models;
using System;
using System.Threading.Tasks;

namespace ArangoDBNetStandardTest.UserApi
{
/// <summary>
/// Provides per-test-class fixture data for <see cref="UserApiClientTest"/>.
/// </summary>
public class UserApiClientTestFixture : ApiClientTestFixtureBase
{
public ArangoDBClient ArangoClient { get; private set; }

public string UsernameToDelete { get; private set; }

public string UsernameToCreate { get; private set; }

public string UsernameExisting { get; private set; }

public UserApiClientTestFixture()
{
ArangoClient = GetArangoDBClient("_system");
UsernameToDelete = nameof(UserApiClientTestFixture) + "Delete";
UsernameToCreate = nameof(UserApiClientTestFixture) + "Post";
UsernameExisting = nameof(UserApiClientTestFixture) + "Existing";
}

public override async Task InitializeAsync()
{
await base.InitializeAsync();

string dbName = nameof(UserApiClientTestFixture);

await CreateDatabase(dbName, new DatabaseUser[]
{
new DatabaseUser()
{
Username = UsernameToDelete
},
new DatabaseUser()
{
Username = UsernameExisting
}
});

_users.Add(UsernameToCreate);
}
}
}
5 changes: 5 additions & 0 deletions arangodb-net-standard/ApiClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public ApiClientBase(IApiClientSerialization serialization)
_serialization = serialization;
}

/// <summary>
/// Gets an <see cref="ApiErrorException"/> from the provided error response.
/// </summary>
/// <param name="response">The error response from ArangoDB.</param>
/// <returns></returns>
protected async Task<ApiErrorException> GetApiErrorException(IApiClientResponse response)
{
var stream = await response.Content.ReadAsStreamAsync();
Expand Down
7 changes: 7 additions & 0 deletions arangodb-net-standard/ArangoDBClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using ArangoDBNetStandard.TransactionApi;
using ArangoDBNetStandard.Transport;
using ArangoDBNetStandard.Transport.Http;
using ArangoDBNetStandard.UserApi;

namespace ArangoDBNetStandard
{
Expand Down Expand Up @@ -64,6 +65,11 @@ public class ArangoDBClient : IDisposable
/// </summary>
public GraphApiClient Graph { get; private set; }

/// <summary>
/// User management API.
/// </summary>
public UserApiClient User { get; private set; }

/// <summary>
/// Create an instance of <see cref="ArangoDBClient"/> from an existing
/// <see cref="HttpClient"/> instance, using the default JSON serialization.
Expand Down Expand Up @@ -125,6 +131,7 @@ private void InitializeApis(
Collection = new CollectionApiClient(transport, serialization);
Transaction = new TransactionApiClient(transport, serialization);
Graph = new GraphApiClient(transport, serialization);
User = new UserApiClient(transport, serialization);
}
}
}
8 changes: 8 additions & 0 deletions arangodb-net-standard/UserApi/IUserApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ namespace ArangoDBNetStandard.UserApi
/// </summary>
public interface IUserApiClient
{
/// <summary>
/// Create a new user. You need server access level Administrate
/// in order to execute this REST call.
/// </summary>
/// <param name="body">The request body containing the user information.</param>
/// <returns></returns>
Task<PostUserResponse> PostUserAsync(PostUserBody body);

Task<DeleteUserResponse> DeleteUserAsync(string username);
}
}
36 changes: 36 additions & 0 deletions arangodb-net-standard/UserApi/Models/PostUserBody.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;

namespace ArangoDBNetStandard.UserApi.Models
{
/// <summary>
/// Represents a request body to create a user.
/// </summary>
public class PostUserBody
{
/// <summary>
/// The name of the user.
/// </summary>
public string User { get; set; }

/// <summary>
/// The user password.
/// If no password is specified, the empty string will be used.
/// If you pass the special value ARANGODB_DEFAULT_ROOT_PASSWORD,
/// then the password will be set the value stored in the environment variable
/// ARANGODB_DEFAULT_ROOT_PASSWORD.
/// This can be used to pass an instance variable into ArangoDB.
/// </summary>
public string Passwd { get; set; }

/// <summary>
/// An optional flag that specifies whether the user is active.
/// If not specified, this will default to true.
/// </summary>
public bool? Active { get; set; }

/// <summary>
/// Optional object with arbitrary extra data about the user.
/// </summary>
public Dictionary<string, object> Extra { get; set; }
}
}
33 changes: 33 additions & 0 deletions arangodb-net-standard/UserApi/Models/PostUserResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Net;

namespace ArangoDBNetStandard.UserApi.Models
{
public class PostUserResponse
{
/// <summary>
/// The name of the user.
/// </summary>
public string User { get; set; }

/// <summary>
/// Whether the user is active.
/// </summary>
public bool Active { get; set; }

/// <summary>
/// Object with arbitrary extra data about the user.
/// </summary>
public Dictionary<string, object> Extra { get; set; }

/// <summary>
/// Indicates whether an error occurred (false in this case).
/// </summary>
public bool Error { get; set; }

/// <summary>
/// The HTTP status code.
/// </summary>
public HttpStatusCode Code { get; set; }
}
}
23 changes: 22 additions & 1 deletion arangodb-net-standard/UserApi/UserApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ public UserApiClient(IApiClientTransport client, IApiClientSerialization seriali
_client = client;
}

/// <summary>
/// Create a new user. You need server access level Administrate
/// in order to execute this REST call.
/// </summary>
/// <exception cref="ApiErrorException">ArangoDB responded with an error.</exception>
/// <param name="body">The request body containing the user information.</param>
/// <returns></returns>
public async Task<PostUserResponse> PostUserAsync(PostUserBody body)
{
var content = GetContent(body, true, true);
using (var response = await _client.PostAsync(_userApiPath, content))
{
if (response.IsSuccessStatusCode)
{
var stream = await response.Content.ReadAsStreamAsync();
return DeserializeJsonFromStream<PostUserResponse>(stream);
}
throw await GetApiErrorException(response);
}
}

/// <summary>
/// Delete a user permanently.
/// You need Administrate for the server access level in order to execute this REST call.
Expand All @@ -54,7 +75,7 @@ public UserApiClient(IApiClientTransport client, IApiClientSerialization seriali
/// <returns></returns>
public virtual async Task<DeleteUserResponse> DeleteUserAsync(string username)
{
string uri = _userApiPath + "/" + WebUtility.HtmlEncode(username);
string uri = _userApiPath + "/" + WebUtility.UrlEncode(username);
using (var response = await _client.DeleteAsync(uri))
{
if (response.IsSuccessStatusCode)
Expand Down

2 comments on commit f1fbee5

@andrewmruddy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for implementing this feature! Is there a plan to push the latest commit to Nuget? Thanks.

@DiscoPYF
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @andrewmruddy , thank you for your message! I think we will probably publish a new release before the end of next month (August 2020). It should contain the full set of endpoints from the user API, which we will be completed by #258 .

Please sign in to comment.