mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
Compare commits
9 Commits
feat/add-b
...
98ef35b532
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98ef35b532 | ||
|
|
f4757979cc | ||
|
|
a580fc6cbd | ||
|
|
00b696b3f0 | ||
|
|
cbaa5bfbca | ||
|
|
9a0eadc514 | ||
|
|
60b784e365 | ||
|
|
95b9d7d52a | ||
|
|
093062f7b2 |
@@ -31,7 +31,6 @@
|
|||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
|
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
|
||||||
<ProjectReference Include="..\..\Service\Service.Breweries\Service.Breweries.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Service\Service.UserManagement\Service.UserManagement.csproj" />
|
<ProjectReference Include="..\..\Service\Service.UserManagement\Service.UserManagement.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace API.Core.Contracts.Breweries;
|
|
||||||
|
|
||||||
public class BreweryCreateDtoValidator : AbstractValidator<BreweryCreateDto>
|
|
||||||
{
|
|
||||||
public BreweryCreateDtoValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.PostedById)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("PostedById is required.");
|
|
||||||
|
|
||||||
RuleFor(x => x.BreweryName)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("Brewery name is required.")
|
|
||||||
.MaximumLength(256)
|
|
||||||
.WithMessage("Brewery name cannot exceed 256 characters.");
|
|
||||||
|
|
||||||
RuleFor(x => x.Description)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("Description is required.")
|
|
||||||
.MaximumLength(512)
|
|
||||||
.WithMessage("Description cannot exceed 512 characters.");
|
|
||||||
|
|
||||||
RuleFor(x => x.Location)
|
|
||||||
.NotNull()
|
|
||||||
.WithMessage("Location is required.");
|
|
||||||
|
|
||||||
RuleFor(x => x.Location.CityId)
|
|
||||||
.NotEmpty()
|
|
||||||
.When(x => x.Location is not null)
|
|
||||||
.WithMessage("CityId is required.");
|
|
||||||
|
|
||||||
RuleFor(x => x.Location.AddressLine1)
|
|
||||||
.NotEmpty()
|
|
||||||
.When(x => x.Location is not null)
|
|
||||||
.WithMessage("Address line 1 is required.")
|
|
||||||
.MaximumLength(256)
|
|
||||||
.When(x => x.Location is not null)
|
|
||||||
.WithMessage("Address line 1 cannot exceed 256 characters.");
|
|
||||||
|
|
||||||
RuleFor(x => x.Location.PostalCode)
|
|
||||||
.NotEmpty()
|
|
||||||
.When(x => x.Location is not null)
|
|
||||||
.WithMessage("Postal code is required.")
|
|
||||||
.MaximumLength(20)
|
|
||||||
.When(x => x.Location is not null)
|
|
||||||
.WithMessage("Postal code cannot exceed 20 characters.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
namespace API.Core.Contracts.Breweries;
|
|
||||||
|
|
||||||
public class BreweryLocationCreateDto
|
|
||||||
{
|
|
||||||
public Guid CityId { get; set; }
|
|
||||||
public string AddressLine1 { get; set; } = string.Empty;
|
|
||||||
public string? AddressLine2 { get; set; }
|
|
||||||
public string PostalCode { get; set; } = string.Empty;
|
|
||||||
public byte[]? Coordinates { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BreweryLocationDto
|
|
||||||
{
|
|
||||||
public Guid BreweryPostLocationId { get; set; }
|
|
||||||
public Guid BreweryPostId { get; set; }
|
|
||||||
public Guid CityId { get; set; }
|
|
||||||
public string AddressLine1 { get; set; } = string.Empty;
|
|
||||||
public string? AddressLine2 { get; set; }
|
|
||||||
public string PostalCode { get; set; } = string.Empty;
|
|
||||||
public byte[]? Coordinates { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BreweryCreateDto
|
|
||||||
{
|
|
||||||
public Guid PostedById { get; set; }
|
|
||||||
public string BreweryName { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public BreweryLocationCreateDto Location { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BreweryDto
|
|
||||||
{
|
|
||||||
public Guid BreweryPostId { get; set; }
|
|
||||||
public Guid PostedById { get; set; }
|
|
||||||
public string BreweryName { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
|
||||||
public byte[]? Timer { get; set; }
|
|
||||||
public BreweryLocationDto? Location { get; set; }
|
|
||||||
}
|
|
||||||
@@ -86,13 +86,6 @@ namespace API.Core.Controllers
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("confirm/resend")]
|
|
||||||
public async Task<ActionResult> ResendConfirmation([FromQuery] Guid userId)
|
|
||||||
{
|
|
||||||
await confirmationService.ResendConfirmationEmailAsync(userId);
|
|
||||||
return Ok(new ResponseBody { Message = "confirmation email has been resent" });
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("refresh")]
|
[HttpPost("refresh")]
|
||||||
public async Task<ActionResult> Refresh(
|
public async Task<ActionResult> Refresh(
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
using API.Core.Contracts.Breweries;
|
|
||||||
using API.Core.Contracts.Common;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Service.Breweries;
|
|
||||||
|
|
||||||
namespace API.Core.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[Authorize(AuthenticationSchemes = "JWT")]
|
|
||||||
public class BreweryController(IBreweryService breweryService) : ControllerBase
|
|
||||||
{
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet("{id:guid}")]
|
|
||||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> GetById(Guid id)
|
|
||||||
{
|
|
||||||
var brewery = await breweryService.GetByIdAsync(id);
|
|
||||||
if (brewery is null)
|
|
||||||
return NotFound(new ResponseBody { Message = $"Brewery with ID {id} not found." });
|
|
||||||
|
|
||||||
return Ok(new ResponseBody<BreweryDto>
|
|
||||||
{
|
|
||||||
Message = "Brewery retrieved successfully.",
|
|
||||||
Payload = MapToDto(brewery),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryDto>>>> GetAll(
|
|
||||||
[FromQuery] int? limit,
|
|
||||||
[FromQuery] int? offset)
|
|
||||||
{
|
|
||||||
var breweries = await breweryService.GetAllAsync(limit, offset);
|
|
||||||
return Ok(new ResponseBody<IEnumerable<BreweryDto>>
|
|
||||||
{
|
|
||||||
Message = "Breweries retrieved successfully.",
|
|
||||||
Payload = breweries.Select(MapToDto),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Create([FromBody] BreweryCreateDto dto)
|
|
||||||
{
|
|
||||||
var request = new BreweryCreateRequest(
|
|
||||||
dto.PostedById,
|
|
||||||
dto.BreweryName,
|
|
||||||
dto.Description,
|
|
||||||
new BreweryLocationCreateRequest(
|
|
||||||
dto.Location.CityId,
|
|
||||||
dto.Location.AddressLine1,
|
|
||||||
dto.Location.AddressLine2,
|
|
||||||
dto.Location.PostalCode,
|
|
||||||
dto.Location.Coordinates
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
var result = await breweryService.CreateAsync(request);
|
|
||||||
if (!result.Success)
|
|
||||||
return BadRequest(new ResponseBody { Message = result.Message });
|
|
||||||
|
|
||||||
return Created($"/api/brewery/{result.Brewery.BreweryPostId}", new ResponseBody<BreweryDto>
|
|
||||||
{
|
|
||||||
Message = "Brewery created successfully.",
|
|
||||||
Payload = MapToDto(result.Brewery),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
|
||||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(Guid id, [FromBody] BreweryDto dto)
|
|
||||||
{
|
|
||||||
if (dto.BreweryPostId != id)
|
|
||||||
return BadRequest(new ResponseBody { Message = "Route ID does not match payload ID." });
|
|
||||||
|
|
||||||
var request = new BreweryUpdateRequest(
|
|
||||||
dto.BreweryPostId,
|
|
||||||
dto.PostedById,
|
|
||||||
dto.BreweryName,
|
|
||||||
dto.Description,
|
|
||||||
dto.Location is null ? null : new BreweryLocationUpdateRequest(
|
|
||||||
dto.Location.BreweryPostLocationId,
|
|
||||||
dto.Location.CityId,
|
|
||||||
dto.Location.AddressLine1,
|
|
||||||
dto.Location.AddressLine2,
|
|
||||||
dto.Location.PostalCode,
|
|
||||||
dto.Location.Coordinates
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
var result = await breweryService.UpdateAsync(request);
|
|
||||||
if (!result.Success)
|
|
||||||
return BadRequest(new ResponseBody { Message = result.Message });
|
|
||||||
|
|
||||||
return Ok(new ResponseBody<BreweryDto>
|
|
||||||
{
|
|
||||||
Message = "Brewery updated successfully.",
|
|
||||||
Payload = MapToDto(result.Brewery),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
|
||||||
public async Task<ActionResult<ResponseBody>> Delete(Guid id)
|
|
||||||
{
|
|
||||||
await breweryService.DeleteAsync(id);
|
|
||||||
return Ok(new ResponseBody { Message = "Brewery deleted successfully." });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BreweryDto MapToDto(Domain.Entities.BreweryPost b) => new()
|
|
||||||
{
|
|
||||||
BreweryPostId = b.BreweryPostId,
|
|
||||||
PostedById = b.PostedById,
|
|
||||||
BreweryName = b.BreweryName,
|
|
||||||
Description = b.Description,
|
|
||||||
CreatedAt = b.CreatedAt,
|
|
||||||
UpdatedAt = b.UpdatedAt,
|
|
||||||
Timer = b.Timer,
|
|
||||||
Location = b.Location is null ? null : new BreweryLocationDto
|
|
||||||
{
|
|
||||||
BreweryPostLocationId = b.Location.BreweryPostLocationId,
|
|
||||||
BreweryPostId = b.Location.BreweryPostId,
|
|
||||||
CityId = b.Location.CityId,
|
|
||||||
AddressLine1 = b.Location.AddressLine1,
|
|
||||||
AddressLine2 = b.Location.AddressLine2,
|
|
||||||
PostalCode = b.Location.PostalCode,
|
|
||||||
Coordinates = b.Location.Coordinates,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
using API.Core;
|
using API.Core;
|
||||||
using API.Core.Authentication;
|
using API.Core.Authentication;
|
||||||
|
using API.Core.Contracts.Common;
|
||||||
|
using Domain.Exceptions;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Infrastructure.Email;
|
using Infrastructure.Email;
|
||||||
|
using Infrastructure.Email.Templates;
|
||||||
using Infrastructure.Email.Templates.Rendering;
|
using Infrastructure.Email.Templates.Rendering;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
using Infrastructure.Repository.Auth;
|
using Infrastructure.Repository.Auth;
|
||||||
using Infrastructure.Repository.Sql;
|
using Infrastructure.Repository.Sql;
|
||||||
using Infrastructure.Repository.UserAccount;
|
using Infrastructure.Repository.UserAccount;
|
||||||
using Infrastructure.Repository.Breweries;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Service.Auth;
|
using Service.Auth;
|
||||||
using Service.Emails;
|
using Service.Emails;
|
||||||
using Service.UserManagement.User;
|
using Service.UserManagement.User;
|
||||||
@@ -50,7 +55,6 @@ builder.Services.AddSingleton<
|
|||||||
|
|
||||||
builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
|
builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
|
||||||
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
|
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
|
||||||
builder.Services.AddScoped<IBreweryRepository, BreweryRepository>();
|
|
||||||
|
|
||||||
builder.Services.AddScoped<IUserService, UserService>();
|
builder.Services.AddScoped<IUserService, UserService>();
|
||||||
builder.Services.AddScoped<ILoginService, LoginService>();
|
builder.Services.AddScoped<ILoginService, LoginService>();
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
<Project Path="Service/Service.Auth.Tests/Service.Auth.Tests.csproj" />
|
<Project Path="Service/Service.Auth.Tests/Service.Auth.Tests.csproj" />
|
||||||
<Project Path="Service/Service.Emails/Service.Emails.csproj" />
|
<Project Path="Service/Service.Emails/Service.Emails.csproj" />
|
||||||
<Project Path="Service/Service.UserManagement/Service.UserManagement.csproj" />
|
<Project Path="Service/Service.UserManagement/Service.UserManagement.csproj" />
|
||||||
<Project Path="Service/Service.Auth/Service.Auth.csproj" />
|
<Project Path="Service\Service.Auth\Service.Auth.csproj" />
|
||||||
<Project Path="Service/Service.Breweries/Service.Breweries.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ USE Biergarten;
|
|||||||
CREATE TABLE dbo.UserAccount
|
CREATE TABLE dbo.UserAccount
|
||||||
(
|
(
|
||||||
UserAccountID UNIQUEIDENTIFIER
|
UserAccountID UNIQUEIDENTIFIER
|
||||||
CONSTRAINT DF_UserAccountID DEFAULT NEWID(),
|
CONSTRAINT DF_UserAccountID DEFAULT NEWID(),
|
||||||
|
|
||||||
Username VARCHAR(64) NOT NULL,
|
Username VARCHAR(64) NOT NULL,
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ CREATE TABLE dbo.UserAccount
|
|||||||
|
|
||||||
UpdatedAt DATETIME,
|
UpdatedAt DATETIME,
|
||||||
|
|
||||||
DateOfBirth DATE NOT NULL,
|
DateOfBirth DATETIME NOT NULL,
|
||||||
|
|
||||||
Timer ROWVERSION,
|
Timer ROWVERSION,
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ CREATE TABLE dbo.UserAccount
|
|||||||
|
|
||||||
CONSTRAINT AK_Email
|
CONSTRAINT AK_Email
|
||||||
UNIQUE (Email)
|
UNIQUE (Email)
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
@@ -108,7 +109,7 @@ CREATE TABLE UserAvatar -- delete avatar photo when user account is deleted
|
|||||||
|
|
||||||
CONSTRAINT AK_UserAvatar_UserAccountID
|
CONSTRAINT AK_UserAvatar_UserAccountID
|
||||||
UNIQUE (UserAccountID)
|
UNIQUE (UserAccountID)
|
||||||
);
|
)
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_UserAvatar_UserAccount
|
CREATE NONCLUSTERED INDEX IX_UserAvatar_UserAccount
|
||||||
ON UserAvatar(UserAccountID);
|
ON UserAvatar(UserAccountID);
|
||||||
@@ -124,7 +125,8 @@ CREATE TABLE UserVerification -- delete verification data when user account is d
|
|||||||
UserAccountID UNIQUEIDENTIFIER NOT NULL,
|
UserAccountID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
VerificationDateTime DATETIME NOT NULL
|
VerificationDateTime DATETIME NOT NULL
|
||||||
CONSTRAINT DF_VerificationDateTime DEFAULT GETDATE(),
|
CONSTRAINT DF_VerificationDateTime
|
||||||
|
DEFAULT GETDATE(),
|
||||||
|
|
||||||
Timer ROWVERSION,
|
Timer ROWVERSION,
|
||||||
|
|
||||||
@@ -153,13 +155,13 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted
|
|||||||
|
|
||||||
UserAccountID UNIQUEIDENTIFIER NOT NULL,
|
UserAccountID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
CreatedAt DATETIME NOT NULL
|
CreatedAt DATETIME
|
||||||
CONSTRAINT DF_UserCredential_CreatedAt DEFAULT GETDATE(),
|
CONSTRAINT DF_UserCredential_CreatedAt DEFAULT GETDATE() NOT NULL,
|
||||||
|
|
||||||
Expiry DATETIME NOT NULL
|
Expiry DATETIME
|
||||||
CONSTRAINT DF_UserCredential_Expiry DEFAULT DATEADD(DAY, 90, GETDATE()),
|
CONSTRAINT DF_UserCredential_Expiry DEFAULT DATEADD(DAY, 90, GETDATE()) NOT NULL,
|
||||||
|
|
||||||
Hash NVARCHAR(256) NOT NULL,
|
Hash NVARCHAR(MAX) NOT NULL,
|
||||||
-- uses argon2
|
-- uses argon2
|
||||||
|
|
||||||
IsRevoked BIT NOT NULL
|
IsRevoked BIT NOT NULL
|
||||||
@@ -175,16 +177,12 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted
|
|||||||
CONSTRAINT FK_UserCredential_UserAccount
|
CONSTRAINT FK_UserCredential_UserAccount
|
||||||
FOREIGN KEY (UserAccountID)
|
FOREIGN KEY (UserAccountID)
|
||||||
REFERENCES UserAccount(UserAccountID)
|
REFERENCES UserAccount(UserAccountID)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE,
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_UserCredential_UserAccount
|
CREATE NONCLUSTERED INDEX IX_UserCredential_UserAccount
|
||||||
ON UserCredential(UserAccountID);
|
ON UserCredential(UserAccountID);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_UserCredential_Account_Active
|
|
||||||
ON UserCredential(UserAccountID, IsRevoked, Expiry)
|
|
||||||
INCLUDE (Hash);
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -197,8 +195,8 @@ CREATE TABLE UserFollow
|
|||||||
|
|
||||||
FollowingID UNIQUEIDENTIFIER NOT NULL,
|
FollowingID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
CreatedAt DATETIME NOT NULL
|
CreatedAt DATETIME
|
||||||
CONSTRAINT DF_UserFollow_CreatedAt DEFAULT GETDATE(),
|
CONSTRAINT DF_UserFollow_CreatedAt DEFAULT GETDATE() NOT NULL,
|
||||||
|
|
||||||
Timer ROWVERSION,
|
Timer ROWVERSION,
|
||||||
|
|
||||||
@@ -207,13 +205,11 @@ CREATE TABLE UserFollow
|
|||||||
|
|
||||||
CONSTRAINT FK_UserFollow_UserAccount
|
CONSTRAINT FK_UserFollow_UserAccount
|
||||||
FOREIGN KEY (UserAccountID)
|
FOREIGN KEY (UserAccountID)
|
||||||
REFERENCES UserAccount(UserAccountID)
|
REFERENCES UserAccount(UserAccountID),
|
||||||
ON DELETE NO ACTION,
|
|
||||||
|
|
||||||
CONSTRAINT FK_UserFollow_UserAccountFollowing
|
CONSTRAINT FK_UserFollow_UserAccountFollowing
|
||||||
FOREIGN KEY (FollowingID)
|
FOREIGN KEY (FollowingID)
|
||||||
REFERENCES UserAccount(UserAccountID)
|
REFERENCES UserAccount(UserAccountID),
|
||||||
ON DELETE NO ACTION,
|
|
||||||
|
|
||||||
CONSTRAINT CK_CannotFollowOwnAccount
|
CONSTRAINT CK_CannotFollowOwnAccount
|
||||||
CHECK (UserAccountID != FollowingID)
|
CHECK (UserAccountID != FollowingID)
|
||||||
@@ -225,6 +221,7 @@ CREATE NONCLUSTERED INDEX IX_UserFollow_UserAccount_FollowingID
|
|||||||
CREATE NONCLUSTERED INDEX IX_UserFollow_FollowingID_UserAccount
|
CREATE NONCLUSTERED INDEX IX_UserFollow_FollowingID_UserAccount
|
||||||
ON UserFollow(FollowingID, UserAccountID);
|
ON UserFollow(FollowingID, UserAccountID);
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -243,7 +240,7 @@ CREATE TABLE Country
|
|||||||
PRIMARY KEY (CountryID),
|
PRIMARY KEY (CountryID),
|
||||||
|
|
||||||
CONSTRAINT AK_Country_ISO3166_1
|
CONSTRAINT AK_Country_ISO3166_1
|
||||||
UNIQUE (ISO3166_1)
|
UNIQUE (ISO3166_1)
|
||||||
);
|
);
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
@@ -302,6 +299,7 @@ CREATE TABLE City
|
|||||||
CREATE NONCLUSTERED INDEX IX_City_StateProvince
|
CREATE NONCLUSTERED INDEX IX_City_StateProvince
|
||||||
ON City(StateProvinceID);
|
ON City(StateProvinceID);
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -310,8 +308,6 @@ CREATE TABLE BreweryPost -- A user cannot be deleted if they have a post
|
|||||||
BreweryPostID UNIQUEIDENTIFIER
|
BreweryPostID UNIQUEIDENTIFIER
|
||||||
CONSTRAINT DF_BreweryPostID DEFAULT NEWID(),
|
CONSTRAINT DF_BreweryPostID DEFAULT NEWID(),
|
||||||
|
|
||||||
BreweryName NVARCHAR(256) NOT NULL,
|
|
||||||
|
|
||||||
PostedByID UNIQUEIDENTIFIER NOT NULL,
|
PostedByID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
Description NVARCHAR(512) NOT NULL,
|
Description NVARCHAR(512) NOT NULL,
|
||||||
@@ -329,15 +325,15 @@ CREATE TABLE BreweryPost -- A user cannot be deleted if they have a post
|
|||||||
CONSTRAINT FK_BreweryPost_UserAccount
|
CONSTRAINT FK_BreweryPost_UserAccount
|
||||||
FOREIGN KEY (PostedByID)
|
FOREIGN KEY (PostedByID)
|
||||||
REFERENCES UserAccount(UserAccountID)
|
REFERENCES UserAccount(UserAccountID)
|
||||||
ON DELETE NO ACTION
|
ON DELETE NO ACTION,
|
||||||
);
|
|
||||||
|
)
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BreweryPost_PostedByID
|
CREATE NONCLUSTERED INDEX IX_BreweryPost_PostedByID
|
||||||
ON BreweryPost(PostedByID);
|
ON BreweryPost(PostedByID);
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
CREATE TABLE BreweryPostLocation
|
CREATE TABLE BreweryPostLocation
|
||||||
(
|
(
|
||||||
BreweryPostLocationID UNIQUEIDENTIFIER
|
BreweryPostLocationID UNIQUEIDENTIFIER
|
||||||
@@ -353,7 +349,7 @@ CREATE TABLE BreweryPostLocation
|
|||||||
|
|
||||||
CityID UNIQUEIDENTIFIER NOT NULL,
|
CityID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
Coordinates GEOGRAPHY NULL,
|
Coordinates GEOGRAPHY NOT NULL,
|
||||||
|
|
||||||
Timer ROWVERSION,
|
Timer ROWVERSION,
|
||||||
|
|
||||||
@@ -366,11 +362,7 @@ CREATE TABLE BreweryPostLocation
|
|||||||
CONSTRAINT FK_BreweryPostLocation_BreweryPost
|
CONSTRAINT FK_BreweryPostLocation_BreweryPost
|
||||||
FOREIGN KEY (BreweryPostID)
|
FOREIGN KEY (BreweryPostID)
|
||||||
REFERENCES BreweryPost(BreweryPostID)
|
REFERENCES BreweryPost(BreweryPostID)
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE
|
||||||
|
|
||||||
CONSTRAINT FK_BreweryPostLocation_City
|
|
||||||
FOREIGN KEY (CityID)
|
|
||||||
REFERENCES City(CityID)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BreweryPostLocation_BreweryPost
|
CREATE NONCLUSTERED INDEX IX_BreweryPostLocation_BreweryPost
|
||||||
@@ -379,18 +371,6 @@ CREATE NONCLUSTERED INDEX IX_BreweryPostLocation_BreweryPost
|
|||||||
CREATE NONCLUSTERED INDEX IX_BreweryPostLocation_City
|
CREATE NONCLUSTERED INDEX IX_BreweryPostLocation_City
|
||||||
ON BreweryPostLocation(CityID);
|
ON BreweryPostLocation(CityID);
|
||||||
|
|
||||||
-- To assess when the time comes:
|
|
||||||
|
|
||||||
-- This would allow for efficient spatial queries to find breweries within a certain distance of a location, but it adds overhead to insert/update operations.
|
|
||||||
|
|
||||||
-- CREATE SPATIAL INDEX SIDX_BreweryPostLocation_Coordinates
|
|
||||||
-- ON BreweryPostLocation(Coordinates)
|
|
||||||
-- USING GEOGRAPHY_GRID
|
|
||||||
-- WITH (
|
|
||||||
-- GRIDS = (LEVEL_1 = MEDIUM, LEVEL_2 = MEDIUM, LEVEL_3 = MEDIUM, LEVEL_4 = MEDIUM),
|
|
||||||
-- CELLS_PER_OBJECT = 16
|
|
||||||
-- );
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -423,14 +403,13 @@ CREATE TABLE BreweryPostPhoto -- All photos linked to a post are deleted if the
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BreweryPostPhoto_Photo_BreweryPost
|
CREATE NONCLUSTERED INDEX IX_BreweryPostPhoto_Photo_BreweryPost
|
||||||
ON BreweryPostPhoto(PhotoID, BreweryPostID);
|
ON BreweryPostPhoto(PhotoID, BreweryPostID);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BreweryPostPhoto_BreweryPost_Photo
|
CREATE NONCLUSTERED INDEX IX_BreweryPostPhoto_BreweryPost_Photo
|
||||||
ON BreweryPostPhoto(BreweryPostID, PhotoID);
|
ON BreweryPostPhoto(BreweryPostID, PhotoID);
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
CREATE TABLE BeerStyle
|
CREATE TABLE BeerStyle
|
||||||
(
|
(
|
||||||
BeerStyleID UNIQUEIDENTIFIER
|
BeerStyleID UNIQUEIDENTIFIER
|
||||||
@@ -465,7 +444,7 @@ CREATE TABLE BeerPost
|
|||||||
-- Alcohol By Volume (typically 0-67%)
|
-- Alcohol By Volume (typically 0-67%)
|
||||||
|
|
||||||
IBU INT NOT NULL,
|
IBU INT NOT NULL,
|
||||||
-- International Bitterness Units (typically 0-120)
|
-- International Bitterness Units (typically 0-100)
|
||||||
|
|
||||||
PostedByID UNIQUEIDENTIFIER NOT NULL,
|
PostedByID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
@@ -485,8 +464,7 @@ CREATE TABLE BeerPost
|
|||||||
|
|
||||||
CONSTRAINT FK_BeerPost_PostedBy
|
CONSTRAINT FK_BeerPost_PostedBy
|
||||||
FOREIGN KEY (PostedByID)
|
FOREIGN KEY (PostedByID)
|
||||||
REFERENCES UserAccount(UserAccountID)
|
REFERENCES UserAccount(UserAccountID),
|
||||||
ON DELETE NO ACTION,
|
|
||||||
|
|
||||||
CONSTRAINT FK_BeerPost_BeerStyle
|
CONSTRAINT FK_BeerPost_BeerStyle
|
||||||
FOREIGN KEY (BeerStyleID)
|
FOREIGN KEY (BeerStyleID)
|
||||||
@@ -544,10 +522,10 @@ CREATE TABLE BeerPostPhoto -- All photos linked to a beer post are deleted if th
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BeerPostPhoto_Photo_BeerPost
|
CREATE NONCLUSTERED INDEX IX_BeerPostPhoto_Photo_BeerPost
|
||||||
ON BeerPostPhoto(PhotoID, BeerPostID);
|
ON BeerPostPhoto(PhotoID, BeerPostID);
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BeerPostPhoto_BeerPost_Photo
|
CREATE NONCLUSTERED INDEX IX_BeerPostPhoto_BeerPost_Photo
|
||||||
ON BeerPostPhoto(BeerPostID, PhotoID);
|
ON BeerPostPhoto(BeerPostID, PhotoID);
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
@@ -561,35 +539,17 @@ CREATE TABLE BeerPostComment
|
|||||||
|
|
||||||
BeerPostID UNIQUEIDENTIFIER NOT NULL,
|
BeerPostID UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
|
||||||
CommentedByID UNIQUEIDENTIFIER NOT NULL,
|
|
||||||
|
|
||||||
Rating INT NOT NULL,
|
Rating INT NOT NULL,
|
||||||
|
|
||||||
CreatedAt DATETIME NOT NULL
|
|
||||||
CONSTRAINT DF_BeerPostComment_CreatedAt DEFAULT GETDATE(),
|
|
||||||
|
|
||||||
UpdatedAt DATETIME NULL,
|
|
||||||
|
|
||||||
Timer ROWVERSION,
|
Timer ROWVERSION,
|
||||||
|
|
||||||
CONSTRAINT PK_BeerPostComment
|
CONSTRAINT PK_BeerPostComment
|
||||||
PRIMARY KEY (BeerPostCommentID),
|
PRIMARY KEY (BeerPostCommentID),
|
||||||
|
|
||||||
CONSTRAINT FK_BeerPostComment_BeerPost
|
CONSTRAINT FK_BeerPostComment_BeerPost
|
||||||
FOREIGN KEY (BeerPostID)
|
FOREIGN KEY (BeerPostID) REFERENCES BeerPost(BeerPostID)
|
||||||
REFERENCES BeerPost(BeerPostID),
|
)
|
||||||
|
|
||||||
CONSTRAINT FK_BeerPostComment_UserAccount
|
|
||||||
FOREIGN KEY (CommentedByID)
|
|
||||||
REFERENCES UserAccount(UserAccountID)
|
|
||||||
ON DELETE NO ACTION,
|
|
||||||
|
|
||||||
CONSTRAINT CHK_BeerPostComment_Rating
|
|
||||||
CHECK (Rating BETWEEN 1 AND 5)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BeerPostComment_BeerPost
|
CREATE NONCLUSTERED INDEX IX_BeerPostComment_BeerPost
|
||||||
ON BeerPostComment(BeerPostID);
|
ON BeerPostComment(BeerPostID)
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_BeerPostComment_CommentedBy
|
|
||||||
ON BeerPostComment(CommentedByID);
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateBrewery(
|
|
||||||
@BreweryName NVARCHAR(256),
|
|
||||||
@Description NVARCHAR(512),
|
|
||||||
@PostedByID UNIQUEIDENTIFIER,
|
|
||||||
@CityID UNIQUEIDENTIFIER,
|
|
||||||
@AddressLine1 NVARCHAR(256),
|
|
||||||
@AddressLine2 NVARCHAR(256) = NULL,
|
|
||||||
@PostalCode NVARCHAR(20),
|
|
||||||
@Coordinates GEOGRAPHY = NULL
|
|
||||||
)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
SET XACT_ABORT ON;
|
|
||||||
|
|
||||||
IF @BreweryName IS NULL
|
|
||||||
THROW 50001, 'Brewery name cannot be null.', 1;
|
|
||||||
|
|
||||||
IF @Description IS NULL
|
|
||||||
THROW 50002, 'Brewery description cannot be null.', 1;
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1
|
|
||||||
FROM dbo.UserAccount
|
|
||||||
WHERE UserAccountID = @PostedByID)
|
|
||||||
THROW 50404, 'User not found.', 1;
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1
|
|
||||||
FROM dbo.City
|
|
||||||
WHERE CityID = @CityID)
|
|
||||||
THROW 50404, 'City not found.', 1;
|
|
||||||
|
|
||||||
DECLARE @NewBreweryID UNIQUEIDENTIFIER = NEWID();
|
|
||||||
DECLARE @NewBrewerLocationID UNIQUEIDENTIFIER = NEWID();
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
INSERT INTO dbo.BreweryPost
|
|
||||||
(BreweryPostID, BreweryName, Description, PostedByID)
|
|
||||||
VALUES (@NewBreweryID, @BreweryName, @Description, @PostedByID);
|
|
||||||
|
|
||||||
INSERT INTO dbo.BreweryPostLocation
|
|
||||||
(BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates)
|
|
||||||
VALUES (@NewBrewerLocationID, @NewBreweryID, @CityID, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates);
|
|
||||||
|
|
||||||
COMMIT TRANSACTION;
|
|
||||||
|
|
||||||
SELECT @NewBreweryID AS BreweryPostID,
|
|
||||||
@NewBrewerLocationID AS BreweryPostLocationID;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CREATE OR ALTER PROCEDURE dbo.USP_GetBreweryById @BreweryPostID UNIQUEIDENTIFIER
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SELECT *
|
|
||||||
FROM BreweryPost bp
|
|
||||||
INNER JOIN BreweryPostLocation bpl
|
|
||||||
ON bp.BreweryPostID = bpl.BreweryPostID
|
|
||||||
WHERE bp.BreweryPostID = @BreweryPostID;
|
|
||||||
END
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace Domain.Entities;
|
|
||||||
|
|
||||||
public class BreweryPost
|
|
||||||
{
|
|
||||||
public Guid BreweryPostId { get; set; }
|
|
||||||
public Guid PostedById { get; set; }
|
|
||||||
public string BreweryName { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
|
||||||
public byte[]? Timer { get; set; }
|
|
||||||
public BreweryPostLocation? Location { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace Domain.Entities;
|
|
||||||
|
|
||||||
public class BreweryPostLocation
|
|
||||||
{
|
|
||||||
public Guid BreweryPostLocationId { get; set; }
|
|
||||||
public Guid BreweryPostId { get; set; }
|
|
||||||
public string AddressLine1 { get; set; } = string.Empty;
|
|
||||||
public string? AddressLine2 { get; set; }
|
|
||||||
public string PostalCode { get; set; } = string.Empty;
|
|
||||||
public Guid CityId { get; set; }
|
|
||||||
public byte[]? Coordinates { get; set; }
|
|
||||||
public byte[]? Timer { get; set; }
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MailKit" Version="4.15.1" />
|
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -17,34 +17,10 @@ public class AuthRepositoryTest
|
|||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
|
conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
|
||||||
.ReturnsScalar(expectedUserId);
|
|
||||||
|
|
||||||
// Mock the subsequent read for the newly created user by id
|
|
||||||
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
|
|
||||||
.ReturnsTable(
|
.ReturnsTable(
|
||||||
MockTable
|
MockTable
|
||||||
.WithColumns(
|
.WithColumns(("UserAccountId", typeof(Guid)))
|
||||||
("UserAccountId", typeof(Guid)),
|
.AddRow(expectedUserId)
|
||||||
("Username", typeof(string)),
|
|
||||||
("FirstName", typeof(string)),
|
|
||||||
("LastName", typeof(string)),
|
|
||||||
("Email", typeof(string)),
|
|
||||||
("CreatedAt", typeof(DateTime)),
|
|
||||||
("UpdatedAt", typeof(DateTime?)),
|
|
||||||
("DateOfBirth", typeof(DateTime)),
|
|
||||||
("Timer", typeof(byte[]))
|
|
||||||
)
|
|
||||||
.AddRow(
|
|
||||||
expectedUserId,
|
|
||||||
"testuser",
|
|
||||||
"Test",
|
|
||||||
"User",
|
|
||||||
"test@example.com",
|
|
||||||
DateTime.UtcNow,
|
|
||||||
null,
|
|
||||||
new DateTime(1990, 1, 1),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
using Apps72.Dev.Data.DbMocker;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Infrastructure.Repository.Breweries;
|
|
||||||
using Infrastructure.Repository.Tests.Database;
|
|
||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace Infrastructure.Repository.Tests.Breweries;
|
|
||||||
|
|
||||||
public class BreweryRepositoryTest
|
|
||||||
{
|
|
||||||
private static BreweryRepository CreateRepo(MockDbConnection conn) =>
|
|
||||||
new(new TestConnectionFactory(conn));
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetByIdAsync_ReturnsBrewery_WhenExists()
|
|
||||||
{
|
|
||||||
var breweryId = Guid.NewGuid();
|
|
||||||
var conn = new MockDbConnection();
|
|
||||||
|
|
||||||
// Repository calls the stored procedure
|
|
||||||
const string getByIdSql = "USP_GetBreweryById";
|
|
||||||
|
|
||||||
var locationId = Guid.NewGuid();
|
|
||||||
|
|
||||||
conn.Mocks.When(cmd => cmd.CommandText == getByIdSql)
|
|
||||||
.ReturnsTable(
|
|
||||||
MockTable
|
|
||||||
.WithColumns(
|
|
||||||
("BreweryPostId", typeof(Guid)),
|
|
||||||
("PostedById", typeof(Guid)),
|
|
||||||
("BreweryName", typeof(string)),
|
|
||||||
("Description", typeof(string)),
|
|
||||||
("CreatedAt", typeof(DateTime)),
|
|
||||||
("UpdatedAt", typeof(DateTime?)),
|
|
||||||
("Timer", typeof(byte[])),
|
|
||||||
("BreweryPostLocationId", typeof(Guid)),
|
|
||||||
("CityId", typeof(Guid)),
|
|
||||||
("AddressLine1", typeof(string)),
|
|
||||||
("AddressLine2", typeof(string)),
|
|
||||||
("PostalCode", typeof(string)),
|
|
||||||
("Coordinates", typeof(byte[]))
|
|
||||||
)
|
|
||||||
.AddRow(
|
|
||||||
breweryId,
|
|
||||||
Guid.NewGuid(),
|
|
||||||
"Test Brewery",
|
|
||||||
"A test brewery description",
|
|
||||||
DateTime.UtcNow,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
locationId,
|
|
||||||
Guid.NewGuid(),
|
|
||||||
"123 Main St",
|
|
||||||
null,
|
|
||||||
"12345",
|
|
||||||
null
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
|
||||||
var result = await repo.GetByIdAsync(breweryId);
|
|
||||||
result.Should().NotBeNull();
|
|
||||||
result!.BreweryPostId.Should().Be(breweryId);
|
|
||||||
result.Location.Should().NotBeNull();
|
|
||||||
result.Location!.BreweryPostLocationId.Should().Be(locationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetByIdAsync_ReturnsNull_WhenNotExists()
|
|
||||||
{
|
|
||||||
var conn = new MockDbConnection();
|
|
||||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_GetBreweryById")
|
|
||||||
.ReturnsTable(MockTable.Empty());
|
|
||||||
var repo = CreateRepo(conn);
|
|
||||||
var result = await repo.GetByIdAsync(Guid.NewGuid());
|
|
||||||
result.Should().BeNull();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateAsync_ExecutesSuccessfully()
|
|
||||||
{
|
|
||||||
var conn = new MockDbConnection();
|
|
||||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_CreateBrewery")
|
|
||||||
.ReturnsScalar(1);
|
|
||||||
var repo = CreateRepo(conn);
|
|
||||||
var brewery = new BreweryPost
|
|
||||||
{
|
|
||||||
BreweryPostId = Guid.NewGuid(),
|
|
||||||
PostedById = Guid.NewGuid(),
|
|
||||||
BreweryName = "Test Brewery",
|
|
||||||
Description = "A test brewery description",
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
Location = new BreweryPostLocation
|
|
||||||
{
|
|
||||||
BreweryPostLocationId = Guid.NewGuid(),
|
|
||||||
CityId = Guid.NewGuid(),
|
|
||||||
AddressLine1 = "123 Main St",
|
|
||||||
PostalCode = "12345",
|
|
||||||
Coordinates = [0x00, 0x01]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Should not throw
|
|
||||||
var act = async () => await repo.CreateAsync(brewery);
|
|
||||||
await act.Should().NotThrowAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,39 +33,18 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
AddParameter(command, "@Hash", passwordHash);
|
AddParameter(command, "@Hash", passwordHash);
|
||||||
|
|
||||||
var result = await command.ExecuteScalarAsync();
|
var result = await command.ExecuteScalarAsync();
|
||||||
|
var userAccountId = result != null ? (Guid)result : Guid.Empty;
|
||||||
|
|
||||||
Guid userAccountId = Guid.Empty;
|
return new Domain.Entities.UserAccount
|
||||||
if (result != null && result != DBNull.Value)
|
|
||||||
{
|
{
|
||||||
if (result is Guid g)
|
UserAccountId = userAccountId,
|
||||||
{
|
Username = username,
|
||||||
userAccountId = g;
|
FirstName = firstName,
|
||||||
}
|
LastName = lastName,
|
||||||
else if (result is string s && Guid.TryParse(s, out var parsed))
|
Email = email,
|
||||||
{
|
DateOfBirth = dateOfBirth,
|
||||||
userAccountId = parsed;
|
CreatedAt = DateTime.UtcNow,
|
||||||
}
|
};
|
||||||
else if (result is byte[] bytes && bytes.Length == 16)
|
|
||||||
{
|
|
||||||
userAccountId = new Guid(bytes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Fallback: try to convert and parse string representation
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var str = result.ToString();
|
|
||||||
if (!string.IsNullOrEmpty(str) && Guid.TryParse(str, out var p))
|
|
||||||
userAccountId = p;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
userAccountId = Guid.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await GetUserByIdAsync(userAccountId) ?? throw new Exception("Failed to retrieve newly registered user.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Domain.Entities.UserAccount?> GetUserByEmailAsync(
|
public async Task<Domain.Entities.UserAccount?> GetUserByEmailAsync(
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
using System.Data.Common;
|
|
||||||
using Domain.Entities;
|
|
||||||
using Infrastructure.Repository.Sql;
|
|
||||||
|
|
||||||
namespace Infrastructure.Repository.Breweries;
|
|
||||||
|
|
||||||
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|
||||||
: Repository<BreweryPost>(connectionFactory), IBreweryRepository
|
|
||||||
{
|
|
||||||
private readonly ISqlConnectionFactory _connectionFactory = connectionFactory;
|
|
||||||
|
|
||||||
public async Task<BreweryPost?> GetByIdAsync(Guid id)
|
|
||||||
{
|
|
||||||
await using var connection = await CreateConnection();
|
|
||||||
await using var command = connection.CreateCommand();
|
|
||||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
|
||||||
|
|
||||||
command.CommandText = "USP_GetBreweryById";
|
|
||||||
AddParameter(command, "@BreweryPostID", id);
|
|
||||||
|
|
||||||
await using var reader = await command.ExecuteReaderAsync();
|
|
||||||
if (await reader.ReadAsync())
|
|
||||||
{
|
|
||||||
return MapToEntity(reader);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task UpdateAsync(BreweryPost brewery)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task DeleteAsync(Guid id)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateAsync(BreweryPost brewery)
|
|
||||||
{
|
|
||||||
await using var connection = await CreateConnection();
|
|
||||||
await using var command = connection.CreateCommand();
|
|
||||||
|
|
||||||
command.CommandText = "USP_CreateBrewery";
|
|
||||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
|
||||||
|
|
||||||
if (brewery.Location is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Location must be provided when creating a brewery.");
|
|
||||||
}
|
|
||||||
|
|
||||||
AddParameter(command, "@BreweryName", brewery.BreweryName);
|
|
||||||
AddParameter(command, "@Description", brewery.Description);
|
|
||||||
AddParameter(command, "@PostedByID", brewery.PostedById);
|
|
||||||
AddParameter(command, "@CityID", brewery.Location?.CityId);
|
|
||||||
AddParameter(command, "@AddressLine1", brewery.Location?.AddressLine1);
|
|
||||||
AddParameter(command, "@AddressLine2", brewery.Location?.AddressLine2);
|
|
||||||
AddParameter(command, "@PostalCode", brewery.Location?.PostalCode);
|
|
||||||
AddParameter(command, "@Coordinates", brewery.Location?.Coordinates);
|
|
||||||
await command.ExecuteNonQueryAsync();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override BreweryPost MapToEntity(DbDataReader reader)
|
|
||||||
{
|
|
||||||
var brewery = new BreweryPost();
|
|
||||||
|
|
||||||
var ordBreweryPostId = reader.GetOrdinal("BreweryPostId");
|
|
||||||
var ordPostedById = reader.GetOrdinal("PostedById");
|
|
||||||
var ordBreweryName = reader.GetOrdinal("BreweryName");
|
|
||||||
var ordDescription = reader.GetOrdinal("Description");
|
|
||||||
var ordCreatedAt = reader.GetOrdinal("CreatedAt");
|
|
||||||
var ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
|
|
||||||
var ordTimer = reader.GetOrdinal("Timer");
|
|
||||||
|
|
||||||
brewery.BreweryPostId = reader.GetGuid(ordBreweryPostId);
|
|
||||||
brewery.PostedById = reader.GetGuid(ordPostedById);
|
|
||||||
brewery.BreweryName = reader.GetString(ordBreweryName);
|
|
||||||
brewery.Description = reader.GetString(ordDescription);
|
|
||||||
brewery.CreatedAt = reader.GetDateTime(ordCreatedAt);
|
|
||||||
|
|
||||||
brewery.UpdatedAt = reader.IsDBNull(ordUpdatedAt) ? null : reader.GetDateTime(ordUpdatedAt);
|
|
||||||
|
|
||||||
// Read timer (varbinary/rowversion) robustly
|
|
||||||
if (reader.IsDBNull(ordTimer))
|
|
||||||
{
|
|
||||||
brewery.Timer = null;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
brewery.Timer = reader.GetFieldValue<byte[]>(ordTimer);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
var length = reader.GetBytes(ordTimer, 0, null, 0, 0);
|
|
||||||
var buffer = new byte[length];
|
|
||||||
reader.GetBytes(ordTimer, 0, buffer, 0, (int)length);
|
|
||||||
brewery.Timer = buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map BreweryPostLocation if columns are present
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
|
|
||||||
if (!reader.IsDBNull(ordLocationId))
|
|
||||||
{
|
|
||||||
var location = new BreweryPostLocation
|
|
||||||
{
|
|
||||||
BreweryPostLocationId = reader.GetGuid(ordLocationId),
|
|
||||||
BreweryPostId = reader.GetGuid(reader.GetOrdinal("BreweryPostId")),
|
|
||||||
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
|
|
||||||
AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")),
|
|
||||||
AddressLine2 = reader.IsDBNull(reader.GetOrdinal("AddressLine2")) ? null : reader.GetString(reader.GetOrdinal("AddressLine2")),
|
|
||||||
PostalCode = reader.GetString(reader.GetOrdinal("PostalCode")),
|
|
||||||
Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates")) ? null : reader.GetFieldValue<byte[]>(reader.GetOrdinal("Coordinates"))
|
|
||||||
};
|
|
||||||
brewery.Location = location;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IndexOutOfRangeException)
|
|
||||||
{
|
|
||||||
// Location columns not present, skip mapping location
|
|
||||||
}
|
|
||||||
|
|
||||||
return brewery;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddParameter(
|
|
||||||
DbCommand command,
|
|
||||||
string name,
|
|
||||||
object? value
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var p = command.CreateParameter();
|
|
||||||
p.ParameterName = name;
|
|
||||||
p.Value = value ?? DBNull.Value;
|
|
||||||
command.Parameters.Add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace Infrastructure.Repository.Breweries;
|
|
||||||
|
|
||||||
public interface IBreweryRepository
|
|
||||||
{
|
|
||||||
Task<BreweryPost?> GetByIdAsync(Guid id);
|
|
||||||
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
|
|
||||||
Task UpdateAsync(BreweryPost brewery);
|
|
||||||
Task DeleteAsync(Guid id);
|
|
||||||
Task CreateAsync(BreweryPost brewery);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Xunit;
|
|
||||||
using Service.Breweries;
|
|
||||||
using API.Core.Contracts.Breweries;
|
|
||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace Service.Breweries.Tests;
|
|
||||||
|
|
||||||
public class BreweryServiceTests
|
|
||||||
{
|
|
||||||
private class FakeRepo : IBreweryRepository
|
|
||||||
{
|
|
||||||
public BreweryPost? Created;
|
|
||||||
|
|
||||||
public Task<BreweryPost?> GetByIdAsync(Guid id) => Task.FromResult<BreweryPost?>(null);
|
|
||||||
public Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset) => Task.FromResult<IEnumerable<BreweryPost>>(Array.Empty<BreweryPost>());
|
|
||||||
public Task UpdateAsync(BreweryPost brewery) { Created = brewery; return Task.CompletedTask; }
|
|
||||||
public Task DeleteAsync(Guid id) => Task.CompletedTask;
|
|
||||||
public Task CreateAsync(BreweryPost brewery) { Created = brewery; return Task.CompletedTask; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateAsync_ReturnsFailure_WhenLocationMissing()
|
|
||||||
{
|
|
||||||
var repo = new FakeRepo();
|
|
||||||
var svc = new BreweryService(repo);
|
|
||||||
|
|
||||||
var dto = new BreweryCreateDto
|
|
||||||
{
|
|
||||||
PostedById = Guid.NewGuid(),
|
|
||||||
BreweryName = "X",
|
|
||||||
Description = "Y",
|
|
||||||
Location = null!
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = await svc.CreateAsync(dto);
|
|
||||||
result.Success.Should().BeFalse();
|
|
||||||
result.Message.Should().Contain("Location");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateAsync_ReturnsSuccess_AndPersistsEntity()
|
|
||||||
{
|
|
||||||
var repo = new FakeRepo();
|
|
||||||
var svc = new BreweryService(repo);
|
|
||||||
|
|
||||||
var loc = new BreweryLocationCreateDto
|
|
||||||
{
|
|
||||||
CityId = Guid.NewGuid(),
|
|
||||||
AddressLine1 = "123 Main",
|
|
||||||
PostalCode = "12345"
|
|
||||||
};
|
|
||||||
|
|
||||||
var dto = new BreweryCreateDto
|
|
||||||
{
|
|
||||||
PostedById = Guid.NewGuid(),
|
|
||||||
BreweryName = "MyBrew",
|
|
||||||
Description = "Desc",
|
|
||||||
Location = loc
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = await svc.CreateAsync(dto);
|
|
||||||
|
|
||||||
result.Success.Should().BeTrue();
|
|
||||||
repo.Created.Should().NotBeNull();
|
|
||||||
repo.Created!.BreweryName.Should().Be("MyBrew");
|
|
||||||
result.Brewery.BreweryName.Should().Be("MyBrew");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<RootNamespace>Service.Breweries.Tests</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
|
||||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Service.Breweries\Service.Breweries.csproj" />
|
|
||||||
<ProjectReference Include="..\Service.Auth\Service.Auth.csproj" />
|
|
||||||
<ProjectReference
|
|
||||||
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
|
||||||
<ProjectReference Include="..\..\API\API.Core\API.Core.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using Infrastructure.Repository.Breweries;
|
|
||||||
|
|
||||||
namespace Service.Breweries;
|
|
||||||
|
|
||||||
public class BreweryService(IBreweryRepository repository) : IBreweryService
|
|
||||||
{
|
|
||||||
public Task<BreweryPost?> GetByIdAsync(Guid id) =>
|
|
||||||
repository.GetByIdAsync(id);
|
|
||||||
|
|
||||||
public Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit = null, int? offset = null) =>
|
|
||||||
repository.GetAllAsync(limit, offset);
|
|
||||||
|
|
||||||
public async Task<BreweryServiceReturn> CreateAsync(BreweryCreateRequest request)
|
|
||||||
{
|
|
||||||
var entity = new BreweryPost
|
|
||||||
{
|
|
||||||
BreweryPostId = Guid.NewGuid(),
|
|
||||||
PostedById = request.PostedById,
|
|
||||||
BreweryName = request.BreweryName,
|
|
||||||
Description = request.Description,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
Location = new BreweryPostLocation
|
|
||||||
{
|
|
||||||
BreweryPostLocationId = Guid.NewGuid(),
|
|
||||||
CityId = request.Location.CityId,
|
|
||||||
AddressLine1 = request.Location.AddressLine1,
|
|
||||||
AddressLine2 = request.Location.AddressLine2,
|
|
||||||
PostalCode = request.Location.PostalCode,
|
|
||||||
Coordinates = request.Location.Coordinates,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await repository.CreateAsync(entity);
|
|
||||||
return new BreweryServiceReturn(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BreweryServiceReturn> UpdateAsync(BreweryUpdateRequest request)
|
|
||||||
{
|
|
||||||
var entity = new BreweryPost
|
|
||||||
{
|
|
||||||
BreweryPostId = request.BreweryPostId,
|
|
||||||
PostedById = request.PostedById,
|
|
||||||
BreweryName = request.BreweryName,
|
|
||||||
Description = request.Description,
|
|
||||||
UpdatedAt = DateTime.UtcNow,
|
|
||||||
Location = request.Location is null ? null : new BreweryPostLocation
|
|
||||||
{
|
|
||||||
BreweryPostLocationId = request.Location.BreweryPostLocationId,
|
|
||||||
BreweryPostId = request.BreweryPostId,
|
|
||||||
CityId = request.Location.CityId,
|
|
||||||
AddressLine1 = request.Location.AddressLine1,
|
|
||||||
AddressLine2 = request.Location.AddressLine2,
|
|
||||||
PostalCode = request.Location.PostalCode,
|
|
||||||
Coordinates = request.Location.Coordinates,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await repository.UpdateAsync(entity);
|
|
||||||
return new BreweryServiceReturn(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task DeleteAsync(Guid id) =>
|
|
||||||
repository.DeleteAsync(id);
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace Service.Breweries;
|
|
||||||
|
|
||||||
public record BreweryCreateRequest(
|
|
||||||
Guid PostedById,
|
|
||||||
string BreweryName,
|
|
||||||
string Description,
|
|
||||||
BreweryLocationCreateRequest Location
|
|
||||||
);
|
|
||||||
|
|
||||||
public record BreweryLocationCreateRequest(
|
|
||||||
Guid CityId,
|
|
||||||
string AddressLine1,
|
|
||||||
string? AddressLine2,
|
|
||||||
string PostalCode,
|
|
||||||
byte[]? Coordinates
|
|
||||||
);
|
|
||||||
|
|
||||||
public record BreweryUpdateRequest(
|
|
||||||
Guid BreweryPostId,
|
|
||||||
Guid PostedById,
|
|
||||||
string BreweryName,
|
|
||||||
string Description,
|
|
||||||
BreweryLocationUpdateRequest? Location
|
|
||||||
);
|
|
||||||
|
|
||||||
public record BreweryLocationUpdateRequest(
|
|
||||||
Guid BreweryPostLocationId,
|
|
||||||
Guid CityId,
|
|
||||||
string AddressLine1,
|
|
||||||
string? AddressLine2,
|
|
||||||
string PostalCode,
|
|
||||||
byte[]? Coordinates
|
|
||||||
);
|
|
||||||
|
|
||||||
public record BreweryServiceReturn
|
|
||||||
{
|
|
||||||
public bool Success { get; init; }
|
|
||||||
public BreweryPost Brewery { get; init; }
|
|
||||||
public string Message { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public BreweryServiceReturn(BreweryPost brewery)
|
|
||||||
{
|
|
||||||
Success = true;
|
|
||||||
Brewery = brewery;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BreweryServiceReturn(string message)
|
|
||||||
{
|
|
||||||
Success = false;
|
|
||||||
Brewery = default!;
|
|
||||||
Message = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IBreweryService
|
|
||||||
{
|
|
||||||
Task<BreweryPost?> GetByIdAsync(Guid id);
|
|
||||||
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit = null, int? offset = null);
|
|
||||||
Task<BreweryServiceReturn> CreateAsync(BreweryCreateRequest request);
|
|
||||||
Task<BreweryServiceReturn> UpdateAsync(BreweryUpdateRequest request);
|
|
||||||
Task DeleteAsync(Guid id);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
Reference in New Issue
Block a user