This commit is contained in:
2018-09-06 18:10:13 +00:00
parent 4fd0ed3179
commit add560830a
5 changed files with 178 additions and 286 deletions

View File

@@ -15,7 +15,7 @@ using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// UserOptions
/// </summary>
@@ -44,15 +44,13 @@ namespace AyaNova.Api.Controllers
}
/// <summary>
/// Get full UserOptions object
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
/// BizAdminFull, BizAdminLimited or be users own record
/// </summary>
/// <param name="id"></param>
/// <param name="id">UserId</param>
/// <returns>A single UserOptions</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetUserOptions([FromRoute] long id)
@@ -62,7 +60,10 @@ namespace AyaNova.Api.Controllers
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
var UserId = UserIdFromContext.Id(HttpContext.Items);
//Different than normal here: a user is *always* allowed to retrieve their own user options object
if (id != UserId && !Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
@@ -73,101 +74,29 @@ namespace AyaNova.Api.Controllers
}
//Instantiate the business object handler
UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
UserOptionsBiz biz = new UserOptionsBiz(ct, UserId, UserRolesFromContext.Roles(HttpContext.Items));
var o = await biz.GetAsync(id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
//Log
EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.UserOptions, AyaEvent.Retrieved), ct);
ct.SaveChanges();
}
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Get paged list of UserOptionss
///
/// Required roles: Any
///
/// </summary>
/// <returns>Paged collection of UserOptionss with paging data</returns>
[HttpGet("ListUserOptionss", Name = nameof(ListUserOptionss))]//We MUST have a "Name" defined or we can't get the link for the pagination, non paged urls don't need a name
public async Task<IActionResult> ListUserOptionss([FromQuery] PagingOptions pagingOptions)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<UserOptions> pr = await biz.GetManyAsync(Url, nameof(ListUserOptionss), pagingOptions);
return Ok(new ApiOkWithPagingResponse<UserOptions>(pr));
}
/// <summary>
/// Get UserOptions pick list
///
/// Required roles: Any
///
/// This list supports querying the Name property
/// include a "q" parameter for string to search for
/// use % for wildcards.
///
/// e.g. q=%Jones%
///
/// Query is case insensitive
/// </summary>
/// <returns>Paged id/name collection of UserOptionss with paging data</returns>
[HttpGet("PickList", Name = nameof(UserOptionsPickList))]
public async Task<IActionResult> UserOptionsPickList([FromQuery] string q, [FromQuery] PagingOptions pagingOptions)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<NameIdItem> pr = await biz.GetPickListAsync(Url, nameof(UserOptionsPickList), pagingOptions, q);
return Ok(new ApiOkWithPagingResponse<NameIdItem>(pr));
}
/// <summary>
/// Put (update) UserOptions
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
/// BizAdminFull or be users own record
///
/// </summary>
/// <param name="id"></param>
/// <param name="id">User id</param>
/// <param name="inObj"></param>
/// <returns></returns>
[HttpPut("{id}")]
@@ -182,15 +111,16 @@ namespace AyaNova.Api.Controllers
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var UserId = UserIdFromContext.Id(HttpContext.Items);
var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id);
var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
if (id != UserId && !Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
@@ -217,15 +147,11 @@ namespace AyaNova.Api.Controllers
}
else
{
//exists but was changed by another user
//I considered returning new and old record, but where would it end?
//Better to let the client decide what to do than to send extra data that is not required
//exists but was changed by another user
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken }));
}
@@ -235,17 +161,15 @@ namespace AyaNova.Api.Controllers
/// Patch (update) UserOptions
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
/// BizAdminFull or be users own record
/// </summary>
/// <param name="id"></param>
/// <param name="id">UserId</param>
/// <param name="concurrencyToken"></param>
/// <param name="objectPatch"></param>
/// <returns></returns>
[HttpPatch("{id}/{concurrencyToken}")]
public async Task<IActionResult> PatchUserOptions([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument<UserOptions> objectPatch)
{
//https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/
{
if (!serverState.IsOpen)
{
@@ -257,18 +181,19 @@ namespace AyaNova.Api.Controllers
return BadRequest(new ApiErrorResponse(ModelState));
}
var UserId = UserIdFromContext.Id(HttpContext.Items);
//Instantiate the business object handler
UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
UserOptionsBiz biz = new UserOptionsBiz(ct, UserId, UserRolesFromContext.Roles(HttpContext.Items));
var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id);
var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
if (id != UserId && !Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
@@ -297,197 +222,18 @@ namespace AyaNova.Api.Controllers
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken }));
}
/// <summary>
/// Post UserOptions
///
/// Required roles:
/// BizAdminFull, InventoryFull, TechFull
/// </summary>
/// <param name="inObj"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostUserOptions([FromBody] UserOptions inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.UserOptions))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
//Create and validate
UserOptions o = await biz.CreateAsync(inObj);
if (o == null)
{
//error return
return BadRequest(new ApiErrorResponse(biz.Errors));
}
else
{
//save to get Id
await ct.SaveChangesAsync();
//Log now that we have the Id
EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.UserOptions, AyaEvent.Created), ct);
await ct.SaveChangesAsync();
//return success and link
return CreatedAtAction("GetUserOptions", new { id = o.Id }, new ApiCreatedResponse(o));
}
}
/// <summary>
/// Delete UserOptions
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
///
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUserOptions([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var dbObj = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.UserOptions, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Delete(dbObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
//Log
EventLogProcessor.DeleteObject(biz.userId, AyaType.UserOptions, dbObj.Id, dbObj.Name, ct);
await ct.SaveChangesAsync();
//Delete children / attached objects
biz.DeleteChildren(dbObj);
return NoContent();
}
private bool UserOptionsExists(long id)
{
return ct.UserOptions.Any(e => e.Id == id);
//NOTE: checks by UserId, NOT by Id as in most other objects
return ct.UserOptions.Any(e => e.UserId == id);
}
/// <summary>
/// Get route that triggers exception for testing
/// </summary>
/// <returns>Nothing, triggers exception</returns>
[HttpGet("exception")]
public ActionResult GetException()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
throw new System.NotSupportedException("Test exception from UserOptions controller");
}
/// <summary>
/// Get route that triggers an alternate type of exception for testing
/// </summary>
/// <returns>Nothing, triggers exception</returns>
[HttpGet("altexception")]
public ActionResult GetAltException()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
throw new System.ArgumentException("Test exception (ALT) from UserOptions controller");
}
/// <summary>
/// Get route that submits a simulated long running operation job for testing
/// </summary>
/// <returns>Nothing</returns>
[HttpGet("TestUserOptionsJob")]
public ActionResult TestUserOptionsJob()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.JobOperations))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Create the job here
OpsJob j = new OpsJob();
j.Name = "TestUserOptionsJob";
j.JobType = JobType.TestUserOptionsJob;
JobsBiz.AddJob(j, ct);
return Accepted(new { JobId = j.GId });//202 accepted
}
//------------

View File

@@ -25,7 +25,8 @@ namespace AyaNova.Biz
AyaNova7Import = 10,
TrialSeeder = 11,
Metrics = 12,
Locale = 13
Locale = 13,
UserOptions=14
}

View File

@@ -23,7 +23,7 @@ namespace AyaNova.Biz
//CHANGE = CREATE, RETRIEVE, UPDATE, DELETE - Full rights
//EDITOWN = special subset of CHANGE: You can create and if it's one you created then you have rights to edit it or delete, but you can't edit ones others have created
//READ = You can read *all* the fields of the record, but can't modify it. Change is automatically checked for so only add different roles from change
//PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly
//PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly
//DELETE = There is no specific delete right for now though it's checked for by routes in Authorized.cs in case we want to add it in future as a separate right from create.
#region All roles initialization
@@ -38,6 +38,19 @@ namespace AyaNova.Biz
ReadFullRecord = AuthorizationRoles.BizAdminLimited
});
////////////////////////////////////////////////////////////
//USEROPTIONS
//(Identical to User, though route also allows own record access full changes)
//
roles.Add(AyaType.UserOptions, new BizRoleSet()
{
Change = AuthorizationRoles.BizAdminFull,
EditOwn = AuthorizationRoles.NoRole,//no one can make a user but a bizadminfull
ReadFullRecord = AuthorizationRoles.BizAdminLimited
});
////////////////////////////////////////////////////////////
//WIDGET
//

View File

@@ -35,8 +35,9 @@ namespace AyaNova.Biz
//Get one
internal async Task<UserOptions> GetAsync(long fetchId)
{
//NOTE: get by UserId as there is a 1:1 relationship, not by useroptions id
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == fetchId);
return await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == fetchId);
}
@@ -49,7 +50,7 @@ namespace AyaNova.Biz
{
//Replace the db object with the PUT object
CopyObject.Copy(inObj, dbObj, "Id");
CopyObject.Copy(inObj, dbObj, "Id, UserId, OwnerId");
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
@@ -65,7 +66,7 @@ namespace AyaNova.Biz
internal bool Patch(UserOptions dbObj, JsonPatchDocument<UserOptions> objectPatch, uint concurrencyToken)
{
//Validate Patch is allowed
if (!ValidateJsonPatch<UserOptions>.Validate(this, objectPatch)) return false;
if (!ValidateJsonPatch<UserOptions>.Validate(this, objectPatch, "UserId")) return false;
//Do the patching
objectPatch.ApplyTo(dbObj);

View File

@@ -0,0 +1,131 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class UserOptionsRu
{
/// <summary>
/// Test all CRUD routes for a UserOptions object
/// </summary>
[Fact]
public async void RU()
{
//CREATE a user
dynamic D1 = new JObject();
D1.name = Util.Uniquify("Test UserOptions User");
D1.ownerId = 1L;
D1.active = true;
D1.login = Util.Uniquify("LOGIN");
D1.password = Util.Uniquify("PASSWORD");
D1.roles = 0;//norole
D1.localeId = 1;//random locale
D1.userType = 3;//non scheduleable
ApiResponse R = await Util.PostAsync("User", await Util.GetTokenAsync("manager", "l3tm3in"), D1.ToString());
Util.ValidateDataReturnResponseOk(R);
long UserId = R.ObjectResponse["result"]["id"].Value<long>();
//Now there should be a user options available for this user
//RETRIEVE companion USEROPTIONS object
//Get it
R = await Util.GetAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
Util.ValidateDataReturnResponseOk(R);
//ensure the default value is set
R.ObjectResponse["result"]["uiColor"].Value<int>().Should().Be(0);
uint concurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value<uint>();
//UPDATE
//PUT
dynamic D2 = new JObject();
D2.emailaddress = "testuseroptions@helloayanova.com";
D2.TimeZoneOffset = -7.5M;//Decimal value
D2.UiColor = -2097216;//Int value (no suffix for int literals)
D2.concurrencyToken = concurrencyToken;
ApiResponse PUTTestResponse = await Util.PutAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D2.ToString());
Util.ValidateHTTPStatusCode(PUTTestResponse, 200);
//VALIDATE
R = await Util.GetAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
Util.ValidateDataReturnResponseOk(R);
//ensure the default value is set
R.ObjectResponse["result"]["emailAddress"].Value<string>().Should().Be(D2.emailaddress.ToString());
R.ObjectResponse["result"]["timeZoneOffset"].Value<decimal>().Should().Be((decimal)D2.TimeZoneOffset);
R.ObjectResponse["result"]["uiColor"].Value<int>().Should().Be((int)D2.UiColor);
concurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value<uint>();
// //update w2id
// D2.name = Util.Uniquify("UPDATED VIA PUT SECOND TEST User");
// D2.OwnerId = 1;
// D2.concurrencyToken = R2.ObjectResponse["result"]["concurrencyToken"].Value<uint>();
// ApiResponse PUTTestResponse = await Util.PutAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D2.ToString());
// Util.ValidateHTTPStatusCode(PUTTestResponse, 200);
// //check PUT worked
// ApiResponse checkPUTWorked = await Util.GetAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
// Util.ValidateNoErrorInResponse(checkPUTWorked);
// checkPUTWorked.ObjectResponse["result"]["name"].Value<string>().Should().Be(D2.name.ToString());
// uint concurrencyToken = PUTTestResponse.ObjectResponse["result"]["concurrencyToken"].Value<uint>();
// //PATCH
// var newName = Util.Uniquify("UPDATED VIA PATCH SECOND TEST User");
// string patchJson = "[{\"value\": \"" + newName + "\",\"path\": \"/name\",\"op\": \"replace\"}]";
// ApiResponse PATCHTestResponse = await Util.PatchAsync("User/" + d2Id.ToString() + "/" + concurrencyToken.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson);
// Util.ValidateHTTPStatusCode(PATCHTestResponse, 200);
// //check PATCH worked
// ApiResponse checkPATCHWorked = await Util.GetAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
// Util.ValidateNoErrorInResponse(checkPATCHWorked);
// checkPATCHWorked.ObjectResponse["result"]["name"].Value<string>().Should().Be(newName);
// //DELETE
// ApiResponse DELETETestResponse = await Util.DeleteAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
// Util.ValidateHTTPStatusCode(DELETETestResponse, 204);
}
/// <summary>
/// Test not found
/// </summary>
[Fact]
public async void GetNonExistentItemShouldError()
{
//Get non existant
//Should return status code 404, api error code 2010
ApiResponse R = await Util.GetAsync("UserOptions/999999", await Util.GetTokenAsync("manager", "l3tm3in"));
Util.ValidateResponseNotFound(R);
}
/// <summary>
/// Test bad modelstate
/// </summary>
[Fact]
public async void GetBadModelStateShouldError()
{
//Get non existant
//Should return status code 400, api error code 2200 and a first target in details of "id"
ApiResponse R = await Util.GetAsync("UserOptions/2q2", await Util.GetTokenAsync("manager", "l3tm3in"));
Util.ValidateBadModelStateResponse(R, "id");
}
//==================================================
}//eoc
}//eons