Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Владимир Бойко, Артем Щеглеватов, Глеб Москалев, Иван Федоров #45

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
14 changes: 7 additions & 7 deletions Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ public static void Main()
var testsToRun = new string[]
{
typeof(Task1_GetUserByIdTests).FullName,
//typeof(Task2_CreateUserTests).FullName,
//typeof(Task3_UpdateUserTests).FullName,
//typeof(Task4_PartiallyUpdateUserTests).FullName,
//typeof(Task5_DeleteUserTests).FullName,
//typeof(Task6_HeadUserByIdTests).FullName,
//typeof(Task7_GetUsersTests).FullName,
//typeof(Task8_GetUsersOptionsTests).FullName,
typeof(Task2_CreateUserTests).FullName,
typeof(Task3_UpdateUserTests).FullName,
typeof(Task4_PartiallyUpdateUserTests).FullName,
typeof(Task5_DeleteUserTests).FullName,
typeof(Task6_HeadUserByIdTests).FullName,
typeof(Task7_GetUsersTests).FullName,
typeof(Task8_GetUsersOptionsTests).FullName,
};
new AutoRun().Execute(new[]
{
Expand Down
4 changes: 2 additions & 2 deletions Tests/UsersApiTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ protected async Task<string> CreateUser(object user)
return createdUserId;
}

protected void DeleteUser(string userId)
protected async Task DeleteUser(string userId)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Delete;
request.RequestUri = BuildUsersByIdUri(userId);
request.Headers.Add("Accept", "*/*");
var response = HttpClient.Send(request);
var response = await HttpClient.SendAsync(request);

response.StatusCode.Should().Be(HttpStatusCode.NoContent);
response.ShouldNotHaveHeader("Content-Type");
Expand Down
262 changes: 255 additions & 7 deletions WebApi.MinimalApi/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using System.Text.RegularExpressions;
using AutoMapper;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using WebApi.MinimalApi.Domain;
using WebApi.MinimalApi.Models;

Expand All @@ -8,20 +13,263 @@ namespace WebApi.MinimalApi.Controllers;
[ApiController]
public class UsersController : Controller
{
// Чтобы ASP.NET положил что-то в userRepository требуется конфигурация
public UsersController(IUserRepository userRepository)
private readonly IUserRepository userRepository;
private readonly IMapper mapper;
private readonly LinkGenerator linkGenerator;

public UsersController(IUserRepository userRepository, IMapper mapper, LinkGenerator linkGenerator)
{
this.userRepository = userRepository;
this.mapper = mapper;
this.linkGenerator = linkGenerator;
}

[HttpGet("{userId}")]
[HttpGet("{userId}", Name = nameof(GetUserById))]
[HttpHead("{userId}")]
public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
{
throw new NotImplementedException();
var userEntity = userRepository.FindById(userId);

if (userEntity is null)
return NotFound();

var userDto = mapper.Map<UserDto>(userEntity);

if (Request.Method == "HEAD")
{
Response.Body = Stream.Null;
}

return Ok(userDto);
}

[HttpPost]
public IActionResult CreateUser([FromBody] object user)
[Produces("application/json", "application/xml")]
public IActionResult CreateUser([FromBody] CreateUserDto user)
{
if (user is null)
{
return BadRequest();
}

if (user.Login == "" || user.Login is null)
{
ModelState.AddModelError("login", "Error");
return UnprocessableEntity(ModelState);
}

if (!user.Login.All(char.IsLetterOrDigit))
{
ModelState.AddModelError("login", "Error");
}

if (!ModelState.IsValid)
{
return UnprocessableEntity(ModelState);
}

var userToCreate = mapper.Map<UserEntity>(user);
var createdUserEntity = userRepository.Insert(userToCreate);

return CreatedAtRoute(
nameof(GetUserById),
new { userId = createdUserEntity.Id },
createdUserEntity.Id);
}

[HttpPut("{userId}")]
[Produces("application/json", "application/xml")]
public IActionResult UpdateUser([FromBody] UpdateUserDto user, [FromRoute] string userId)
{
if (user is null)
{
return BadRequest();
}

var validationResult = ValidateUserDto(user);
if (validationResult != null)
{
return validationResult;
}

var userToUpdate = mapper.Map<UserEntity>(user);

if (!Guid.TryParse(userId, out var guid))
{
return BadRequest();
}

var userEntity = userRepository.FindById(guid);

if (userEntity == null)
{
var createdUserEntity = userRepository.Insert(userToUpdate);

return CreatedAtRoute(
nameof(GetUserById),
new { userId = createdUserEntity.Id },
createdUserEntity.Id);
}

userRepository.Update(userToUpdate);

return NoContent();
}

[HttpPatch("{userId}")]

public IActionResult PartiallyUpdateUser([FromBody] JsonPatchDocument<UpdateUserDto> patchDoc, [FromRoute] String userId)
{
if (patchDoc == null)
{
return BadRequest();
}

var validationResult = ValidatePatchDocument(patchDoc);
if (validationResult != null)
{
return validationResult;
}

if (!Guid.TryParse(userId, out var guid))
{
return NotFound();
}

var user = userRepository.FindById(guid);
if (user == null)
{
return NotFound();
}

var updateUserDto = mapper.Map<UpdateUserDto>(user);
patchDoc.ApplyTo(updateUserDto, ModelState);
TryValidateModel(updateUserDto);

return ModelState.IsValid ? NoContent() : UnprocessableEntity(ModelState);
}

[HttpDelete("{userId}")]
public IActionResult DeleteUser(string userId)
{
if (!Guid.TryParse(userId, out var guid))
{
return NotFound();
}

if (userRepository.FindById(guid) == null)
{
return NotFound();
}

userRepository.Delete(guid);

return NoContent();
}

[HttpGet]
public IActionResult GetUsers([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
if (pageNumber < 1)
pageNumber = 1;

if (pageSize < 1)
pageSize = 1;

if (pageSize > 20)
pageSize = 20;

var pageList = userRepository.GetPage(pageNumber, pageSize);
var users = mapper.Map<IEnumerable<UserDto>>(pageList);

var paginationHeader = new
{
previousPageLink = pageList.HasPrevious ?
linkGenerator.GetUriByAction(
HttpContext, nameof(GetUsers),
values: new { pageNumber = pageNumber - 1, pageSize }) : null,
nextPageLink = pageList.HasNext ?
linkGenerator.GetUriByAction(
HttpContext, nameof(GetUsers),
values: new { pageNumber = pageNumber + 1, pageSize }) : null,
totalCount = pageList.TotalCount,
pageSize = pageSize,
currentPage = pageNumber,
totalPages = (int)Math.Ceiling((double)pageList.TotalCount / pageSize)
};
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(paginationHeader));

return Ok(users);
}

[HttpOptions]
public IActionResult Options()
{
throw new NotImplementedException();
Response.Headers.Add("Allow", "GET, POST, OPTIONS");

return Ok();
}

private IActionResult ValidateUserDto(UpdateUserDto userDto)
{
if (string.IsNullOrWhiteSpace(userDto.Login) || !userDto.Login.All(char.IsLetterOrDigit))
{
ModelState.AddModelError("login", "Login must contain only letters and digits and cannot be empty.");
return UnprocessableEntity(ModelState);
}

if (string.IsNullOrWhiteSpace(userDto.FirstName))
{
ModelState.AddModelError("firstName", "First name cannot be empty.");
return UnprocessableEntity(ModelState);
}

if (string.IsNullOrWhiteSpace(userDto.LastName))
{
ModelState.AddModelError("lastName", "Last name cannot be empty.");
return UnprocessableEntity(ModelState);
}

return null;
}

private IActionResult ValidatePatchDocument(JsonPatchDocument<UpdateUserDto> patchDoc)
{
foreach (var operation in patchDoc.Operations)
{
if (operation.path == "login")
{
if (ContainsSpecialCharacters(operation.value.ToString()))
{
ModelState.AddModelError("login", "Login must not contain special characters.");
return UnprocessableEntity(ModelState);
}
if (string.IsNullOrWhiteSpace(operation.value.ToString()))
{
ModelState.AddModelError("login", "Login cannot be empty.");
return UnprocessableEntity(ModelState);
}
}
else if (operation.path == "firstName" && string.IsNullOrWhiteSpace(operation.value.ToString()))
{
ModelState.AddModelError("firstName", "First name cannot be empty.");
return UnprocessableEntity(ModelState);
}
else if (operation.path == "lastName" && string.IsNullOrWhiteSpace(operation.value.ToString()))
{
ModelState.AddModelError("lastName", "Last name cannot be empty.");
return UnprocessableEntity(ModelState);
}
}
return null;
}

static bool ContainsSpecialCharacters(string str)
{
// регулярное выражение для проверки на наличие специальных символов
var pattern = @"[^a-zA-Z0-9а-яА-ЯёЁ]";

// Проверяем, соответствует ли строка шаблону
return Regex.IsMatch(str, pattern);
}
}
16 changes: 16 additions & 0 deletions WebApi.MinimalApi/Models/CreateUserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace WebApi.MinimalApi.Models;

public class CreateUserDto
{
[Required]
public string Login { get; set; }

[DefaultValue("a")]
public string FirstName { get; set; }

[DefaultValue("b")]
public string LastName { get; set; }
}
13 changes: 13 additions & 0 deletions WebApi.MinimalApi/Models/UpdateUserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;

namespace WebApi.MinimalApi.Models;

public class UpdateUserDto
{
[Required]
[RegularExpression("^[0-9\\p{L}]*$",
ErrorMessage = "Login should contain only letters or digits")]
public string Login { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Loading