Updated to current conventions (#11)
* Updated to current conventions -Changed startup and testing to generic host. -Changed exception handling from GlobalExceptionFilter to startup middleware. -Changed DTO validation to IValidatableObject. -Replaced TestStartup with generic host web application factory. -Implemented current testing conventions, folder structure, integration tests, unit tests, etc. -Implemented DTO validation tests. -Created Constants class. -General code cleanup. * Docker file and client updates -Created dockerfile to build client and service. -Added @fluentui/react package to client. * Completed upgrade to fluent and packages updates -Completely updated App.tsx to fluent by replacing Customizer with a React.Fragment. -Removed office-ui-fabric-core and office-ui-fabric-react packages which are now rolled into the @fluentui/react package. -Updated to latest packages with npm update. * Updated dependencies for deployment and dockerfile -Moved package dependencies into devDependencies to optimize deployment. -Fixed dockerfile comment. Co-authored-by: Ben Werle <v-bewerl@microsoft.com>
This commit is contained in:
Родитель
711d7441ca
Коммит
a61bc99e04
|
@ -0,0 +1,11 @@
|
|||
.dockerignore
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
.vs
|
||||
.vscode
|
||||
docker-compose.yml
|
||||
docker-compose.*.yml
|
||||
*/bin
|
||||
*/obj
|
||||
*/node_modules
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -3,77 +3,18 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.4.3",
|
||||
"@svgr/webpack": "4.1.0",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/node": "12.0.2",
|
||||
"@types/node-fetch": "^2.3.7",
|
||||
"@types/react": "16.8.18",
|
||||
"@types/react-adal": "^0.4.1",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react-redux": "^7.1.1",
|
||||
"@types/react-router-dom": "^4.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "1.6.0",
|
||||
"@typescript-eslint/parser": "1.6.0",
|
||||
"@uifabric/fluent-theme": "^7.0.2",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-jest": "^24.8.0",
|
||||
"babel-loader": "8.0.5",
|
||||
"babel-plugin-named-asset-import": "^0.3.2",
|
||||
"babel-preset-react-app": "^9.0.0",
|
||||
"@fluentui/react": "^7.123.7",
|
||||
"@uifabric/fluent-theme": "^7.1.115",
|
||||
"camelcase": "^5.2.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.2.0",
|
||||
"css-loader": "^1.0.1",
|
||||
"dotenv": "6.2.0",
|
||||
"dotenv-expand": "4.2.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-react-app": "^4.0.1",
|
||||
"eslint-loader": "2.1.2",
|
||||
"eslint-plugin-flowtype": "2.50.1",
|
||||
"eslint-plugin-import": "2.16.0",
|
||||
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||
"eslint-plugin-react": "7.12.4",
|
||||
"eslint-plugin-react-hooks": "^1.5.0",
|
||||
"file-loader": "3.0.1",
|
||||
"fs-extra": "7.0.1",
|
||||
"html-webpack-plugin": "4.0.0-beta.5",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"jest": "24.7.1",
|
||||
"jest-environment-jsdom-fourteen": "0.1.0",
|
||||
"jest-resolve": "24.7.1",
|
||||
"jest-watch-typeahead": "0.3.0",
|
||||
"mini-css-extract-plugin": "0.5.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"office-ui-fabric-core": "^10.1.0",
|
||||
"office-ui-fabric-react": "^7.7.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"pnp-webpack-plugin": "1.2.1",
|
||||
"postcss-flexbugs-fixes": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-normalize": "7.0.1",
|
||||
"postcss-preset-env": "6.6.0",
|
||||
"postcss-safe-parser": "4.0.1",
|
||||
"react": "^16.8.6",
|
||||
"react": "^16.13.1",
|
||||
"react-adal": "^0.4.24",
|
||||
"react-app-polyfill": "^1.0.1",
|
||||
"react-dev-utils": "^9.0.1",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-redux": "^7.1.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"resolve": "1.10.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"semver": "6.0.0",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "1.2.3",
|
||||
"ts-pnp": "1.1.2",
|
||||
"typescript": "3.4.5",
|
||||
"typings-for-css-modules-loader": "^1.7.0",
|
||||
"url-loader": "1.1.2",
|
||||
"webpack": "4.29.6",
|
||||
"webpack-dev-server": "3.2.1",
|
||||
"webpack-manifest-plugin": "2.0.4",
|
||||
"workbox-webpack-plugin": "4.2.0"
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^4.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
|
@ -147,6 +88,64 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-jest": "^24.0.2"
|
||||
"@babel/core": "7.4.3",
|
||||
"@svgr/webpack": "4.1.0",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/node": "12.0.2",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/react": "16.8.18",
|
||||
"@types/react-adal": "^0.4.2",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-router-dom": "^4.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "1.6.0",
|
||||
"@typescript-eslint/parser": "1.6.0",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "8.0.5",
|
||||
"babel-plugin-named-asset-import": "^0.3.6",
|
||||
"babel-preset-react-app": "^9.1.2",
|
||||
"case-sensitive-paths-webpack-plugin": "2.2.0",
|
||||
"css-loader": "^1.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-react-app": "^4.0.1",
|
||||
"eslint-loader": "2.1.2",
|
||||
"eslint-plugin-flowtype": "2.50.1",
|
||||
"eslint-plugin-import": "2.16.0",
|
||||
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||
"eslint-plugin-react": "7.12.4",
|
||||
"eslint-plugin-react-hooks": "^1.7.0",
|
||||
"file-loader": "3.0.1",
|
||||
"html-webpack-plugin": "4.0.0-beta.5",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"jest": "24.7.1",
|
||||
"jest-environment-jsdom-fourteen": "0.1.0",
|
||||
"jest-resolve": "24.7.1",
|
||||
"jest-watch-typeahead": "0.3.0",
|
||||
"mini-css-extract-plugin": "0.5.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"pnp-webpack-plugin": "1.2.1",
|
||||
"postcss-flexbugs-fixes": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-normalize": "7.0.1",
|
||||
"postcss-preset-env": "6.6.0",
|
||||
"postcss-safe-parser": "4.0.1",
|
||||
"react-dev-utils": "^9.1.0",
|
||||
"resolve": "1.10.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
"semver": "6.0.0",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "1.2.3",
|
||||
"ts-jest": "^24.3.0",
|
||||
"ts-pnp": "1.1.2",
|
||||
"typescript": "3.4.5",
|
||||
"typings-for-css-modules-loader": "^1.7.0",
|
||||
"url-loader": "1.1.2",
|
||||
"webpack": "4.29.6",
|
||||
"webpack-dev-server": "3.2.1",
|
||||
"webpack-manifest-plugin": "2.0.4",
|
||||
"workbox-webpack-plugin": "4.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import 'office-ui-fabric-core/dist/css/fabric.css';
|
||||
import 'office-ui-fabric-react/dist/css/fabric.css';
|
||||
|
||||
import { FluentCustomizations } from '@uifabric/fluent-theme';
|
||||
import {
|
||||
Customizer,
|
||||
IButtonProps,
|
||||
Icon,
|
||||
Image,
|
||||
initializeIcons,
|
||||
Nav,
|
||||
Text
|
||||
} from 'office-ui-fabric-react';
|
||||
} from '@fluentui/react';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Link, Route } from 'react-router-dom';
|
||||
|
||||
|
@ -24,7 +22,7 @@ initializeIcons();
|
|||
const App: React.FC = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Customizer {...FluentCustomizations}>
|
||||
<React.Fragment>
|
||||
<div className="ms-Grid" dir="ltr">
|
||||
<div className="ms-Grid-row">
|
||||
<div className={styles.header}>
|
||||
|
@ -66,7 +64,7 @@ const App: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Customizer>
|
||||
</React.Fragment>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
SpinnerSize
|
||||
} from 'office-ui-fabric-react';
|
||||
} from '@fluentui/react';
|
||||
import { IGroupDto, ApiClient } from '../../generated/backend';
|
||||
|
||||
const Groups: React.FC = () => {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# to build local image from PowerShell (e.g. when you're authoring this dockerfile):
|
||||
# docker build -f .\dockerfile . --build-arg BUILDCONFIG=dev
|
||||
|
||||
# build backend
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS serviceBuild
|
||||
|
||||
# copy backend files into 'service' folder in prep for dotnet commands
|
||||
COPY service /service
|
||||
|
||||
# ensure tests are passing
|
||||
ARG ASPNETCORE_ENVIRONMENT
|
||||
WORKDIR /service/Microsoft.DSX.ProjectTemplate.API
|
||||
RUN dotnet build
|
||||
|
||||
# setup frontend
|
||||
FROM node:12.16.1 AS clientBuild
|
||||
ARG APP_ENV
|
||||
RUN echo APP_ENV = ${APP_ENV}
|
||||
RUN npm config set unsafe-perm true
|
||||
COPY client /client
|
||||
|
||||
# copy auto-generated TS files from API bulid
|
||||
COPY --from=serviceBuild /client/src/generated/. client/src/generated/
|
||||
|
||||
# build frontend
|
||||
WORKDIR /client
|
||||
RUN npm i
|
||||
ENV REACT_APP_ENV=${APP_ENV}
|
||||
RUN npm run build
|
||||
|
||||
# copy our frontend into published app's wwwroot folder
|
||||
FROM serviceBuild AS publisher
|
||||
COPY --from=clientBuild /client/build /app/wwwroot/
|
||||
|
||||
# build & publish our API projectARG ClientId
|
||||
ARG ASPNETCORE_ENVIRONMENT
|
||||
RUN dotnet publish /service/Microsoft.DSX.ProjectTemplate.API/Microsoft.DSX.ProjectTemplate.API.csproj -c Release -o /app
|
||||
|
||||
# build runtime image (contains full stack)
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
COPY --from=publisher /app ./
|
||||
ENTRYPOINT ["dotnet", "Microsoft.DSX.ProjectTemplate.API.dll"]
|
|
@ -1,13 +1,21 @@
|
|||
namespace Microsoft.DSX.ProjectTemplate.API.Controllers
|
||||
{
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.API.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Base controller for our web API.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Produces("application/json")]
|
||||
public abstract class BaseController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets injected Mediator instance.
|
||||
/// </summary>
|
||||
protected IMediator Mediator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseController"/> class.
|
||||
/// </summary>
|
||||
|
@ -17,10 +25,5 @@
|
|||
{
|
||||
Mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets injected Mediator instance.
|
||||
/// </summary>
|
||||
protected IMediator Mediator { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,66 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.DSX.ProjectTemplate.Command.Group;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
/// <summary>
|
||||
/// Controller for Group APIs.
|
||||
/// </summary>
|
||||
public class GroupsController : BaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="mediator">Mediator instance from dependency injection.</param>
|
||||
public GroupsController(IMediator mediator) : base(mediator) { }
|
||||
|
||||
/// <summary>
|
||||
/// Get all Groups.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<GroupDto>>> GetAllGroups()
|
||||
{
|
||||
return Ok(await Mediator.Send(new GetAllGroupsQuery()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a Group by its Id.
|
||||
/// </summary>
|
||||
/// <param name="id">ID of the Group to get.</param>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<GroupDto>> GetGroup(int id)
|
||||
{
|
||||
return Ok(await Mediator.Send(new GetGroupByIdQuery() { GroupId = id }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new Group.
|
||||
/// </summary>
|
||||
/// <param name="dto">A Group DTO.</param>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<GroupDto>> CreateGroup([FromBody] GroupDto dto)
|
||||
{
|
||||
return Ok(await Mediator.Send(new CreateGroupCommand() { Group = dto }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing Group.
|
||||
/// </summary>
|
||||
/// <param name="dto">Updated Group DTO.</param>
|
||||
[HttpPut]
|
||||
public async Task<ActionResult<GroupDto>> UpdateGroup([FromBody] GroupDto dto)
|
||||
{
|
||||
return Ok(await Mediator.Send(new UpdateGroupCommand() { Group = dto }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an existing Group.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Group to be deleted.</param>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult<GroupDto>> DeleteGroup([FromRoute] int id)
|
||||
{
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.API
|
||||
{
|
||||
/// <summary>
|
||||
/// This filter will capture unhandled exceptions that occur in
|
||||
/// controller creation, model binding, action filters, or action methods.
|
||||
/// </summary>
|
||||
/// <remarks>https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#exception-filters</remarks>
|
||||
public class GlobalExceptionFilter : IExceptionFilter
|
||||
{
|
||||
private readonly IHostEnvironment _hostingEnvironment;
|
||||
private readonly ILogger<GlobalExceptionFilter> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GlobalExceptionFilter"/> class.
|
||||
/// </summary>
|
||||
public GlobalExceptionFilter(IHostEnvironment hostingEnvironment, ILogger<GlobalExceptionFilter> logger)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
// transform the exception into JSON
|
||||
if (context.Exception is ExceptionBase)
|
||||
{
|
||||
_logger.LogInformation(context.Exception, context.ActionDescriptor.DisplayName);
|
||||
|
||||
ExceptionBase customException = context.Exception as ExceptionBase;
|
||||
|
||||
var exceptionJson = new ErrorResponseDto
|
||||
{
|
||||
Message = customException.Message,
|
||||
InnerExceptionMessage = customException.InnerException?.Message,
|
||||
};
|
||||
|
||||
switch (_hostingEnvironment.EnvironmentName.ToLowerInvariant())
|
||||
{
|
||||
case "local":
|
||||
case "dev":
|
||||
case "development":
|
||||
exceptionJson.StackTrace = customException.StackTrace;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
context.Result = new JsonResult(exceptionJson) { StatusCode = (int)customException.StatusCode };
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// unexpected exception so log to _logger (which probably will be Application Insights)
|
||||
_logger.LogCritical(context.Exception, context.ActionDescriptor.DisplayName);
|
||||
|
||||
// unhandled exception keeps percolating and will be handled by ASP.NET Core
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,24 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.API
|
||||
{
|
||||
/// <summary>
|
||||
/// The main class of the application.
|
||||
/// </summary>
|
||||
public class Program
|
||||
{
|
||||
|
||||
{
|
||||
/// <summary>
|
||||
/// Main method where execution begins.
|
||||
/// </summary>
|
||||
/// <param name="args">Injected by the operating system.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
||||
//var webHost = CreateWebHostBuilder(args).Build();
|
||||
|
||||
IHost host = CreateHostBuilder(args).Build();
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
|
@ -29,27 +31,35 @@ namespace Microsoft.DSX.ProjectTemplate.API
|
|||
{
|
||||
logger.LogCritical(ex, ex.Message);
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Creates a host builder for this API.
|
||||
/// </summary>
|
||||
/// <param name="args">Arguments for the web host builder.</param>
|
||||
/// <returns>A fully configured host builder.</returns>
|
||||
public static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
return Host
|
||||
.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
|
||||
private static void RunDatabaseMigrations(IHost host, ILogger logger)
|
||||
{
|
||||
logger.LogInformation($"Running database migrations");
|
||||
logger.LogInformation($"Running database migrations.");
|
||||
|
||||
using (var serviceScope = host.Services.CreateScope())
|
||||
{
|
||||
var context = serviceScope.ServiceProvider.GetRequiredService<ProjectTemplateDbContext>();
|
||||
context.Database.Migrate();
|
||||
}
|
||||
logger.LogInformation($"Completed database migrations");
|
||||
|
||||
logger.LogInformation($"Completed database migrations.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Abstractions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Exceptions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Startup extensions.
|
||||
/// </summary>
|
||||
public static class StartupExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Register the database connections used by the API with DI.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddDbConnections(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment)
|
||||
{
|
||||
/*
|
||||
* NOTE: All database queries against strings are case-insensitive unless collation is set against
|
||||
* the database, table, or column. This cannot be overridden by the client and the results are
|
||||
* evaluated locally (e.g. .Equals(string, comparison)).
|
||||
* We should avoid client evaluation where possible!
|
||||
* See - https://github.com/aspnet/EntityFrameworkCore/issues/1222#issuecomment-443119582
|
||||
*/
|
||||
|
||||
if (environment.IsEnvironment(Constants.Environments.Test))
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
return services.AddDbContext<ProjectTemplateDbContext>(options => options
|
||||
.UseSqlServer(configuration.GetConnectionString("Database")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the mapping profiles.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddAutoMapperProfiles(this IServiceCollection services)
|
||||
{
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfile>());
|
||||
return services.AddSingleton(config.CreateMapper());
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the services used by the API with DI
|
||||
/// </summary>
|
||||
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddOptions()
|
||||
.AddSingleton<IEmailService, EmailService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure Cross-Origin Request Sharing (CORS) options.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCors(this IServiceCollection services)
|
||||
{
|
||||
return services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("CorsPolicy",
|
||||
builder =>
|
||||
{
|
||||
builder
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure project template swagger document.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSwaggerDocument(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddSwaggerDocument(config =>
|
||||
{
|
||||
config.PostProcess = document =>
|
||||
{
|
||||
document.Info.Version = "v1";
|
||||
document.Info.Title = "Project Template API";
|
||||
document.Info.Contact = new NSwag.OpenApiContact
|
||||
{
|
||||
Name = "Devices Software Experiences"
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startup extension method to add our custom exception handling.
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app)
|
||||
{
|
||||
return app.UseExceptionHandler(options =>
|
||||
{
|
||||
options.Run(async context =>
|
||||
{
|
||||
var pathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
|
||||
using var scope = context.RequestServices.CreateScope();
|
||||
|
||||
var hostEnvironment = scope.ServiceProvider.GetRequiredService<IHostEnvironment>();
|
||||
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger(pathFeature?.Error.GetType());
|
||||
logger.LogError(pathFeature?.Error, pathFeature?.Path);
|
||||
|
||||
object exceptionJson = hostEnvironment.EnvironmentName.ToLower() switch
|
||||
{
|
||||
var env when
|
||||
env == Constants.Environments.Local ||
|
||||
env == Constants.Environments.Test ||
|
||||
env == Constants.Environments.Dev ||
|
||||
env == Constants.Environments.Development => new { pathFeature?.Error.Message, pathFeature?.Error.StackTrace },
|
||||
_ => new { pathFeature?.Error.Message }
|
||||
};
|
||||
|
||||
// if we threw the exception, ensure the response contains the right status code
|
||||
context.Response.StatusCode = pathFeature?.Error is ExceptionBase ? (int)(pathFeature.Error as ExceptionBase)?.StatusCode : 500;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(exceptionJson), context.RequestAborted);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,8 @@
|
|||
using AutoMapper;
|
||||
using MediatR;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.DSX.ProjectTemplate.Command;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Abstractions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
@ -15,76 +10,48 @@ using Microsoft.Extensions.Hosting;
|
|||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
||||
namespace Microsoft.DSX.ProjectTemplate.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that initializes our API.
|
||||
/// </summary>
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration, IHostEnvironment hostingEnvironment)
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IHostEnvironment _environment;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Startup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Configuration of the web API.</param>
|
||||
/// <param name="environment">Hosting environment.</param>
|
||||
public Startup(IConfiguration configuration, IHostEnvironment environment)
|
||||
{
|
||||
Configuration = configuration;
|
||||
HostingEnvironment = hostingEnvironment;
|
||||
_configuration = configuration;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
readonly string CorsPolicy = "CorsPolicy";
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
public IHostEnvironment HostingEnvironment { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
/// <summary>
|
||||
/// This method gets called by the runtime and is used to add services to the DI container.
|
||||
/// </summary>
|
||||
/// <param name="services">Collection of services to be provided by DI.</param>
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<GlobalExceptionFilter>();
|
||||
|
||||
services
|
||||
.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(CorsPolicy,
|
||||
builder => builder.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader());
|
||||
})
|
||||
.AddDbConnections(_configuration, _environment)
|
||||
.AddAutoMapperProfiles()
|
||||
.AddServices()
|
||||
.AddMediatR(typeof(HandlerBase))
|
||||
.AddCors()
|
||||
.AddSwaggerDocument()
|
||||
.AddControllers();
|
||||
|
||||
// Register Entity Framework Core
|
||||
ConfigureDatabase(services);
|
||||
|
||||
// Register MediatR and handlers
|
||||
services.AddMediatR(typeof(HandlerBase));
|
||||
|
||||
// Register AutoMapper
|
||||
var mapperConfig = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfile>());
|
||||
services.AddSingleton(mapperConfig.CreateMapper());
|
||||
|
||||
// Register our custom services.
|
||||
services.AddSingleton<IEmailService, EmailService>();
|
||||
|
||||
// Register the Swagger services
|
||||
services.AddSwaggerDocument(config =>
|
||||
{
|
||||
config.PostProcess = document =>
|
||||
{
|
||||
document.Info.Version = "v1";
|
||||
document.Info.Title = "Project Template API";
|
||||
document.Info.Contact = new NSwag.OpenApiContact
|
||||
{
|
||||
Name = "Devices Software Experiences",
|
||||
Email = "dsx@microsoft.com",
|
||||
Url = "https://deviceswiki.com/wiki/DSX"
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void ConfigureDatabase(IServiceCollection services)
|
||||
/// <summary>
|
||||
/// This method gets called by the runtime and is used to configure the HTTP request pipeline.
|
||||
/// </summary>
|
||||
/// <param name="app">Application builder.</param>
|
||||
public virtual void Configure(IApplicationBuilder app)
|
||||
{
|
||||
services.AddDbContext<ProjectTemplateDbContext>(options =>
|
||||
{
|
||||
options.UseSqlServer(Configuration.GetConnectionString("Database"));
|
||||
});
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
if (_environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
@ -94,25 +61,14 @@ namespace Microsoft.DSX.ProjectTemplate.API
|
|||
app.UseHsts();
|
||||
}
|
||||
|
||||
// Register the Swagger generator and the Swagger UI middlewares
|
||||
app.UseOpenApi();
|
||||
app.UseSwaggerUi3();
|
||||
|
||||
if (!env.IsEnvironment("Test"))
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseCors(CorsPolicy);
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
app
|
||||
.UseExceptionHandling()
|
||||
.UseOpenApi()
|
||||
.UseSwaggerUi3()
|
||||
.UseRouting()
|
||||
.UseCors("CorsPolicy")
|
||||
.UseAuthorization()
|
||||
.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.API
|
||||
{
|
||||
public class TestStartup : Startup
|
||||
{
|
||||
public TestStartup(IConfiguration configuration, IHostEnvironment hostingEnvironment)
|
||||
: base(configuration, hostingEnvironment)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ConfigureDatabase(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddDbContext<ProjectTemplateDbContext>(options =>
|
||||
options.UseInMemoryDatabase("InMemoryTestDatabase"), ServiceLifetime.Singleton);
|
||||
|
||||
services
|
||||
.AddTransient<TestDataSeeder>();
|
||||
}
|
||||
|
||||
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
// perform all configuration in the normal startup
|
||||
base.Configure(app, env);
|
||||
|
||||
// seed the database with test data
|
||||
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
|
||||
{
|
||||
var seeder = serviceScope.ServiceProvider.GetService<TestDataSeeder>();
|
||||
seeder.SeedTestData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ using Microsoft.DSX.ProjectTemplate.Data;
|
|||
namespace Microsoft.DSX.ProjectTemplate.Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that all command handlers inherit from
|
||||
/// Base class of all command handlers.
|
||||
/// </summary>
|
||||
public abstract class CommandHandlerBase : HandlerBase
|
||||
{
|
||||
|
|
|
@ -30,23 +30,13 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
|
||||
public async Task<GroupDto> Handle(CreateGroupCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Group == null)
|
||||
{
|
||||
throw new BadRequestException($"A valid {nameof(Data.Models.Group)} must be provided");
|
||||
}
|
||||
|
||||
if (!request.Group.IsValid())
|
||||
{
|
||||
throw new BadRequestException(request.Group.GetValidationErrors());
|
||||
}
|
||||
|
||||
var dto = request.Group;
|
||||
|
||||
bool nameAlreadyUsed = await Database.Groups
|
||||
.AnyAsync(e => e.Name.Trim() == dto.Name.Trim());
|
||||
if (nameAlreadyUsed)
|
||||
{
|
||||
throw new BadRequestException($"{nameof(dto.Name)} '{dto.Name}' already used");
|
||||
throw new BadRequestException($"{nameof(dto.Name)} '{dto.Name}' already used.");
|
||||
}
|
||||
|
||||
var model = new Data.Models.Group()
|
||||
|
|
|
@ -30,14 +30,14 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
{
|
||||
if (request.GroupId <= 0)
|
||||
{
|
||||
throw new BadRequestException($"A valid {nameof(Data.Models.Group)} Id must be provided");
|
||||
throw new BadRequestException($"A valid {nameof(Data.Models.Group)} Id must be provided.");
|
||||
}
|
||||
|
||||
var group = await Database.Groups.FindAsync(request.GroupId);
|
||||
|
||||
if (group == null)
|
||||
{
|
||||
throw new EntityNotFoundException($"{nameof(Data.Models.Group)} not found");
|
||||
throw new EntityNotFoundException($"{nameof(Data.Models.Group)} not found.");
|
||||
}
|
||||
|
||||
Database.Groups.Remove(group);
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
{
|
||||
if (request.GroupId <= 0)
|
||||
{
|
||||
throw new BadRequestException($"A valid {nameof(Data.Models.Group)} Id must be provided");
|
||||
throw new BadRequestException($"A valid {nameof(Data.Models.Group)} Id must be provided.");
|
||||
}
|
||||
|
||||
var innerResult = await Database.Groups
|
||||
|
@ -53,7 +53,7 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
|
||||
if (innerResult == null)
|
||||
{
|
||||
throw new EntityNotFoundException($"{nameof(Data.Models.Group)} with Id {request.GroupId} cannot be found");
|
||||
throw new EntityNotFoundException($"{nameof(Data.Models.Group)} with Id {request.GroupId} cannot be found.");
|
||||
}
|
||||
|
||||
return Mapper.Map<GroupDto>(innerResult);
|
||||
|
|
|
@ -34,11 +34,11 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
// As a subscriber, we run on the thread pool so we need to handle our own failures appropriately.
|
||||
try
|
||||
{
|
||||
await _emailService.SendEmailAsync("a@microsoft.com", "b@microsoft.com", $"New group '{notification.Group.Name}' was created", "lorem ipsum");
|
||||
await _emailService.SendEmailAsync("a@microsoft.com", "b@microsoft.com", $"New group '{notification.Group.Name}' was created.", "lorem ipsum");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send email");
|
||||
_logger.LogError(ex, "Failed to send email.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,16 +30,6 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
|
||||
public async Task<GroupDto> Handle(UpdateGroupCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Group == null)
|
||||
{
|
||||
throw new BadRequestException($"A valid {nameof(Data.Models.Group)} object must be provided");
|
||||
}
|
||||
|
||||
if (!request.Group.IsValid())
|
||||
{
|
||||
throw new BadRequestException(request.Group.GetValidationErrors());
|
||||
}
|
||||
|
||||
var dto = request.Group;
|
||||
|
||||
if (dto.Id <= 0)
|
||||
|
@ -53,7 +43,7 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
|
||||
if (model == null)
|
||||
{
|
||||
throw new EntityNotFoundException($"{nameof(Data.Models.Group)} with Id {dto.Id} not found");
|
||||
throw new EntityNotFoundException($"{nameof(Data.Models.Group)} with Id {dto.Id} not found.");
|
||||
}
|
||||
|
||||
// ensure uniqueness of name
|
||||
|
@ -61,7 +51,7 @@ namespace Microsoft.DSX.ProjectTemplate.Command.Group
|
|||
.AnyAsync(e => e.Name.Trim() == dto.Name.Trim()) && dto.Name != (model.Name);
|
||||
if (nameAlreadyUsed)
|
||||
{
|
||||
throw new BadRequestException($"{nameof(dto.Name)} {dto.Name} already used");
|
||||
throw new BadRequestException($"{nameof(dto.Name)} {dto.Name} already used.");
|
||||
}
|
||||
|
||||
model.Name = dto.Name;
|
||||
|
|
|
@ -6,7 +6,7 @@ using Microsoft.DSX.ProjectTemplate.Data;
|
|||
namespace Microsoft.DSX.ProjectTemplate.Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that all handlers inherit from
|
||||
/// Base class of all handlers.
|
||||
/// </summary>
|
||||
public abstract class HandlerBase
|
||||
{
|
||||
|
|
|
@ -6,7 +6,7 @@ using Microsoft.DSX.ProjectTemplate.Data;
|
|||
namespace Microsoft.DSX.ProjectTemplate.Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that all query handlers inherit from
|
||||
/// Base class of all query handlers.
|
||||
/// </summary>
|
||||
public abstract class QueryHandlerBase : HandlerBase
|
||||
{
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
namespace Microsoft.DSX.ProjectTemplate.Data
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public static class Environments
|
||||
{
|
||||
public const string Local = "local";
|
||||
public const string Test = "test";
|
||||
public const string Dev = "dev";
|
||||
public const string Development = "development";
|
||||
}
|
||||
|
||||
public static class MaximumLengths
|
||||
{
|
||||
public const int StringColumn = 512;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.DTOs
|
|||
public abstract class AuditDto<TType> : BaseDto<TType>
|
||||
{
|
||||
public DateTime CreatedDate { get; set; }
|
||||
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Data.DTOs
|
||||
{
|
||||
public abstract class BaseDto<TType>
|
||||
public abstract class BaseDto<TType> : IValidatableObject
|
||||
{
|
||||
public TType Id { get; set; }
|
||||
|
||||
protected ModelStateDictionary ModelState { get; } = new ModelStateDictionary();
|
||||
|
||||
public abstract bool IsValid();
|
||||
|
||||
public virtual string GetValidationErrors()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (var error in ModelState)
|
||||
{
|
||||
sb.AppendLine($"{error.Key} : {error.Value}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
protected ModelStateDictionary ModelState { get; } = new ModelStateDictionary();
|
||||
|
||||
public abstract IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
namespace Microsoft.DSX.ProjectTemplate.Data.DTOs
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Data.DTOs
|
||||
{
|
||||
public class GroupDto : AuditDto<int>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public override bool IsValid()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
ModelState.AddModelError(nameof(Name), $"{nameof(Name)} cannot be null or empty.");
|
||||
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
if (Name.Length > Constants.MaximumLengths.StringColumn)
|
||||
{
|
||||
yield return new ValidationResult($"{nameof(Name)} must be less than {Constants.MaximumLengths.StringColumn} characters.", new[] { nameof(Name) });
|
||||
}
|
||||
}
|
||||
|
||||
return ModelState.IsValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ValidationResult($"{nameof(Name)} cannot be null or empty.", new[] { nameof(Name) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Events
|
|||
{
|
||||
public class GroupCreatedDomainEvent : INotification
|
||||
{
|
||||
public Group Group { get; }
|
||||
|
||||
public GroupCreatedDomainEvent(Group group)
|
||||
{
|
||||
Group = group;
|
||||
}
|
||||
|
||||
public Group Group { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Exceptions
|
|||
{
|
||||
private static string DefaultMessageHeader => "Bad Request";
|
||||
|
||||
public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest;
|
||||
|
||||
public BadRequestException(string message, string messageHeader = null)
|
||||
: base(message, messageHeader ?? DefaultMessageHeader) { }
|
||||
|
||||
public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Exceptions
|
|||
{
|
||||
private static string DefaultMessageHeader => "Not found";
|
||||
|
||||
public override HttpStatusCode StatusCode => HttpStatusCode.NotFound;
|
||||
|
||||
public EntityNotFoundException(string message, string messageHeader = null)
|
||||
: base(message, messageHeader ?? DefaultMessageHeader) { }
|
||||
|
||||
public override HttpStatusCode StatusCode => HttpStatusCode.NotFound;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,14 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Exceptions
|
|||
{
|
||||
public abstract class ExceptionBase : Exception
|
||||
{
|
||||
public abstract HttpStatusCode StatusCode { get; }
|
||||
|
||||
public string MessageHeader { get; }
|
||||
|
||||
protected ExceptionBase(string message, string messageHeader = null)
|
||||
: base(message)
|
||||
{
|
||||
MessageHeader = messageHeader;
|
||||
}
|
||||
|
||||
public abstract HttpStatusCode StatusCode { get; }
|
||||
|
||||
public string MessageHeader { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<DateTime>("UpdatedDate");
|
||||
|
||||
|
@ -84,7 +84,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
b.Property<DateTime>("CreatedDate");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<DateTime>("UpdatedDate");
|
||||
|
||||
|
@ -111,7 +111,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<int?>("OwnerId");
|
||||
|
||||
|
@ -140,7 +140,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
b.Property<int>("GroupId");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<DateTime>("UpdatedDate");
|
||||
|
||||
|
@ -160,7 +160,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
b.Property<DateTime>("CreatedDate");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<string>("Metadata");
|
||||
|
||||
|
@ -187,22 +187,22 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b1.Property<string>("LocationAddressLine1")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationAddressLine2")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationCity")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationCountry")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationStateProvince")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationZipCode")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.HasKey("LibraryId");
|
||||
|
||||
|
|
|
@ -16,13 +16,13 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
|
||||
CreatedDate = table.Column<DateTime>(nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(nullable: false),
|
||||
Name = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Address_LocationAddressLine1 = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Address_LocationAddressLine2 = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Address_LocationCity = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Address_LocationStateProvince = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Address_LocationZipCode = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Address_LocationCountry = table.Column<string>(maxLength: 512, nullable: true)
|
||||
Name = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Address_LocationAddressLine1 = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Address_LocationAddressLine2 = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Address_LocationCity = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Address_LocationStateProvince = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Address_LocationZipCode = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Address_LocationCountry = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
|
||||
CreatedDate = table.Column<DateTime>(nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(nullable: false),
|
||||
DisplayName = table.Column<string>(maxLength: 512, nullable: true),
|
||||
DisplayName = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
Metadata = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
@ -53,7 +53,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
|
||||
CreatedDate = table.Column<DateTime>(nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(nullable: false),
|
||||
Name = table.Column<string>(maxLength: 512, nullable: false),
|
||||
Name = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: false),
|
||||
IsActive = table.Column<bool>(nullable: false),
|
||||
DefaultLibraryId = table.Column<int>(nullable: true)
|
||||
},
|
||||
|
@ -76,7 +76,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
|
||||
CreatedDate = table.Column<DateTime>(nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(nullable: false),
|
||||
Name = table.Column<string>(maxLength: 512, nullable: false),
|
||||
Name = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: false),
|
||||
IsActive = table.Column<bool>(nullable: false),
|
||||
GroupId = table.Column<int>(nullable: false),
|
||||
OwnerId = table.Column<int>(nullable: true)
|
||||
|
@ -106,7 +106,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
|
||||
CreatedDate = table.Column<DateTime>(nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(nullable: false),
|
||||
Name = table.Column<string>(maxLength: 512, nullable: true),
|
||||
Name = table.Column<string>(maxLength: Constants.MaximumLengths.StringColumn, nullable: true),
|
||||
GroupId = table.Column<int>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<DateTime>("UpdatedDate");
|
||||
|
||||
|
@ -82,7 +82,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
b.Property<DateTime>("CreatedDate");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<DateTime>("UpdatedDate");
|
||||
|
||||
|
@ -109,7 +109,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<int?>("OwnerId");
|
||||
|
||||
|
@ -138,7 +138,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
b.Property<int>("GroupId");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<DateTime>("UpdatedDate");
|
||||
|
||||
|
@ -158,7 +158,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
b.Property<DateTime>("CreatedDate");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b.Property<string>("Metadata");
|
||||
|
||||
|
@ -185,22 +185,22 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Migrations
|
|||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b1.Property<string>("LocationAddressLine1")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationAddressLine2")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationCity")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationCountry")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationStateProvince")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.Property<string>("LocationZipCode")
|
||||
.HasMaxLength(512);
|
||||
.HasMaxLength(Constants.MaximumLengths.StringColumn);
|
||||
|
||||
b1.HasKey("LibraryId");
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Data.Models
|
||||
{
|
||||
[Owned]
|
||||
public class Address
|
||||
{
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string LocationAddressLine1 { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string LocationAddressLine2 { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string LocationCity { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string LocationStateProvince { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string LocationZipCode { get; set; }
|
||||
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string LocationCountry { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Models
|
|||
public class Group : AuditModel<int>
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Models
|
|||
{
|
||||
public class Library : AuditModel<int>
|
||||
{
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string Name { get; set; }
|
||||
|
||||
public Address Address { get; set; }
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Models
|
|||
public class Project : AuditModel<int>
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Models
|
|||
{
|
||||
public class Team : AuditModel<int>
|
||||
{
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string Name { get; set; }
|
||||
|
||||
public virtual Group Group { get; set; }
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Models
|
|||
{
|
||||
public class User : AuditModel<int>
|
||||
{
|
||||
[MaxLength(512)]
|
||||
[MaxLength(Constants.MaximumLengths.StringColumn)]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; }
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
//using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Data
|
||||
{
|
||||
|
|
|
@ -15,13 +15,13 @@ namespace Microsoft.DSX.ProjectTemplate.Data.Utilities
|
|||
|
||||
public void SeedTestData()
|
||||
{
|
||||
_logger.LogInformation("Database seeding started");
|
||||
_logger.LogInformation("Database seeding started.");
|
||||
|
||||
SeedGroups(10);
|
||||
|
||||
SeedProjects(10);
|
||||
|
||||
_logger.LogInformation("Database seeding completed");
|
||||
_logger.LogInformation("Database seeding completed.");
|
||||
}
|
||||
|
||||
private void SeedGroups(int entityCount)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test
|
||||
{
|
||||
public class CustomWebApplicationFactory<TStartup>
|
||||
: WebApplicationFactory<TStartup>
|
||||
where TStartup : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Code inside this method runs _before_ <see cref="TStartup"/> does.
|
||||
/// </summary>
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
|
||||
builder
|
||||
.UseEnvironment("Test")
|
||||
.UseStartup<TStartup>();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupControllerTests : IntegrationTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task GetAll_Valid_Success()
|
||||
{
|
||||
// Arrange
|
||||
var client = Factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/groups");
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<IEnumerable<GroupDto>>(response);
|
||||
result.Should().HaveCountGreaterThan(0);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
public async Task GetById_Valid_Success(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
var client = Factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<GroupDto>(response);
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().Be(groupId);
|
||||
result.IsValid().Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Create_Valid_Success()
|
||||
{
|
||||
// Arrange
|
||||
var client = Factory.CreateClient();
|
||||
var dto = SetupGroupDto();
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<GroupDto>(response);
|
||||
result.Id.Should().BeGreaterThan(0);
|
||||
result.Name.Should().Be(dto.Name);
|
||||
result.IsActive.Should().Be(dto.IsActive);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Update_Valid_Success()
|
||||
{
|
||||
// Arrange
|
||||
var client = Factory.CreateClient();
|
||||
var dto = SetupGroupDto();
|
||||
dto.Id = 4;
|
||||
|
||||
// Act
|
||||
var response = await client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<GroupDto>(response);
|
||||
result.Id.Should().Be(dto.Id);
|
||||
result.Name.Should().Be(dto.Name);
|
||||
result.IsActive.Should().Be(dto.IsActive);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(4)]
|
||||
public async Task Delete_Valid_Success(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
var client = Factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.DeleteAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<bool>(response);
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
private GroupDto SetupGroupDto()
|
||||
{
|
||||
return new GroupDto()
|
||||
{
|
||||
Name = RandomFactory.GetCompanyName(),
|
||||
IsActive = RandomFactory.GetBoolean()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Command.Group;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Events;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Exceptions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupCreateCommandHandlerTests : UnitTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
public async Task Create_MissingName_BadRequestException(string name)
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new CreateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var dto = SeedHelper.CreateValidNewGroupDto(db, Mapper);
|
||||
dto.Name = name;
|
||||
|
||||
return handler.Handle(new CreateGroupCommand() { Group = dto }, default);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
public async Task Create_NameAlreadyUsed_BadRequestException()
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new CreateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var existingGroup = SeedHelper.GetRandomGroup(db);
|
||||
var dto = SeedHelper.CreateValidNewGroupDto(db, Mapper, existingGroup.Name);
|
||||
|
||||
return handler.Handle(new CreateGroupCommand() { Group = dto }, default);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Create_Valid_PublishesGroupCreatedDomainEvent()
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new CreateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var dto = SeedHelper.CreateValidNewGroupDto(db, Mapper);
|
||||
|
||||
return handler.Handle(new CreateGroupCommand() { Group = dto }, default);
|
||||
}, (result, db) =>
|
||||
{
|
||||
MockMediator.Verify(x => x.Publish(It.IsAny<GroupCreatedDomainEvent>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Command.Group;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Events;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupCreatedEventHandlerTests : UnitTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("Alpha")]
|
||||
public async Task Event_Created_SendsEmail(string groupName)
|
||||
{
|
||||
await ExecuteWithDb(async (db) =>
|
||||
{
|
||||
var handler = new GroupCreatedEventHandler(MockMediator.Object, db, Mapper,
|
||||
MockAuthorizationService.Object, MockEmailService.Object,
|
||||
LoggerFactory.CreateLogger<GroupCreatedEventHandler>());
|
||||
|
||||
var group = SeedHelper.CreateValidNewGroup(db, groupName);
|
||||
var notification = new GroupCreatedDomainEvent(group);
|
||||
|
||||
await handler.Handle(notification, default);
|
||||
return Task.CompletedTask;
|
||||
}, (_, db) =>
|
||||
{
|
||||
MockEmailService.Verify(x => x.SendEmailAsync(It.IsNotNull<string>(),
|
||||
It.IsNotNull<string>(),
|
||||
It.Is<string>(subject => subject.Contains(groupName)),
|
||||
It.IsNotNull<string>()),
|
||||
Times.Once);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Command.Group;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Exceptions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupDeleteCommandHandlerTests : UnitTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
[DataRow(0)]
|
||||
[DataRow(-1)]
|
||||
public async Task Delete_InvalidId_BadRequestException(int id)
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new DeleteGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
return handler.Handle(new DeleteGroupCommand() { GroupId = id }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(EntityNotFoundException))]
|
||||
public async Task Delete_IdNotFound_EntityNotFoundException()
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new DeleteGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var lastGroup = db.Groups.OrderBy(o => o.Id).Last();
|
||||
|
||||
return handler.Handle(new DeleteGroupCommand() { GroupId = (lastGroup.Id + 1) }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.DSX.ProjectTemplate.Command.Group;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Exceptions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupQueryHandlerTests : UnitTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task GetAll_Valid_Success()
|
||||
{
|
||||
DateTime dtStart = DateTime.Now;
|
||||
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new GroupQueryHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
return handler.Handle(new GetAllGroupsQuery(), default(CancellationToken));
|
||||
}, (result, db) =>
|
||||
{
|
||||
result.Should().NotBeNull();
|
||||
result.Should().BeAssignableTo<IEnumerable<GroupDto>>();
|
||||
result.Should().HaveCountGreaterThan(0);
|
||||
foreach (var group in result)
|
||||
{
|
||||
group.Id.Should().BeGreaterThan(0);
|
||||
group.Name.Should().NotBeNullOrWhiteSpace();
|
||||
group.CreatedDate.Should().BeOnOrAfter(dtStart);
|
||||
group.UpdatedDate.Should().BeOnOrAfter(dtStart);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
[DataRow(0)]
|
||||
[DataRow(-1)]
|
||||
public async Task GetById_InvalidId_BadRequestException(int id)
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new GroupQueryHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
return handler.Handle(new GetGroupByIdQuery() { GroupId = id }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[ExpectedException(typeof(EntityNotFoundException))]
|
||||
[DataRow(int.MaxValue)]
|
||||
public async Task GetById_IdNotFound_EntityNotFoundException(int id)
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new GroupQueryHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
return handler.Handle(new GetGroupByIdQuery() { GroupId = id }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Command.Group;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Exceptions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupUpdateCommandHandlerTests : UnitTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
[DataRow(0)]
|
||||
[DataRow(-1)]
|
||||
public async Task Update_InvalidId_BadRequestException(int id)
|
||||
{
|
||||
await ExecuteWithDb(async (db) =>
|
||||
{
|
||||
var handler = new UpdateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
dto.Id = id;
|
||||
|
||||
return await handler.Handle(new UpdateGroupCommand() { Group = dto }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
public async Task Update_MissingName_BadRequestException(string name)
|
||||
{
|
||||
await ExecuteWithDb(async (db) =>
|
||||
{
|
||||
var handler = new UpdateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
|
||||
dto.Name = name;
|
||||
|
||||
return await handler.Handle(new UpdateGroupCommand() { Group = dto }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Update_NoChanges_Success()
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new UpdateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var existingGroup = SeedHelper.GetRandomGroup(db);
|
||||
var dto = Mapper.Map<GroupDto>(existingGroup);
|
||||
|
||||
return handler.Handle(new UpdateGroupCommand() { Group = dto }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(BadRequestException))]
|
||||
public async Task Update_NameAlreadyUsed_BadRequestException()
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new UpdateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var existingGroup = SeedHelper.GetRandomGroup(db);
|
||||
var differentGroup = db.Groups
|
||||
.Where(x => x.Id != existingGroup.Id)
|
||||
.OrderBy(x => Guid.NewGuid())
|
||||
.First();
|
||||
var dto = Mapper.Map<GroupDto>(existingGroup);
|
||||
dto.Name = differentGroup.Name;
|
||||
|
||||
return handler.Handle(new UpdateGroupCommand() { Group = dto }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(EntityNotFoundException))]
|
||||
public async Task Update_IdNotFound_EntityNotFoundException()
|
||||
{
|
||||
await ExecuteWithDb((db) =>
|
||||
{
|
||||
var handler = new UpdateGroupCommandHandler(
|
||||
MockMediator.Object,
|
||||
db,
|
||||
Mapper,
|
||||
MockAuthorizationService.Object);
|
||||
|
||||
var dto = SeedHelper.CreateValidNewGroupDto(db, Mapper);
|
||||
dto.Id = int.MaxValue;
|
||||
|
||||
return handler.Handle(new UpdateGroupCommand() { Group = dto }, default(CancellationToken));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Data.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Infrastructure
|
||||
{
|
||||
internal class FakeEmailService : IEmailService
|
||||
{
|
||||
private int m_sentCount;
|
||||
|
||||
public FakeEmailService()
|
||||
{
|
||||
m_sentCount = 0;
|
||||
}
|
||||
|
||||
public Task SendEmailAsync(string from, string to, string subject, string body)
|
||||
{
|
||||
m_sentCount++;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public int GetSentCount()
|
||||
{
|
||||
return m_sentCount;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Abstractions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for bootstrapping an application into memory for functional end-to-end testing.
|
||||
/// </summary>
|
||||
public class ProjectTemplateWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
|
||||
{
|
||||
private readonly Guid _dbGuid = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of an <see cref="IHostBuilder"/> and adds to its pre-configured defaults
|
||||
/// </summary>
|
||||
protected override IHostBuilder CreateHostBuilder()
|
||||
{
|
||||
return Host
|
||||
.CreateDefaultBuilder()
|
||||
.UseEnvironment(Constants.Environments.Test)
|
||||
.ConfigureLogging(loggingBuilder =>
|
||||
{
|
||||
loggingBuilder
|
||||
.ClearProviders()
|
||||
.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Trace);
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder
|
||||
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
|
||||
.CaptureStartupErrors(true)
|
||||
.UseStartup<TStartup>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the <see cref="IWebHostBuilder"/> services and sets up the in-memory database
|
||||
/// </summary>
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder
|
||||
.ConfigureTestServices(services =>
|
||||
{
|
||||
services
|
||||
.AddSingleton<IEmailService, FakeEmailService>()
|
||||
.RemoveAll(typeof(IHostedService));
|
||||
})
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
var serviceProvider = new ServiceCollection()
|
||||
.AddEntityFrameworkInMemoryDatabase()
|
||||
.BuildServiceProvider();
|
||||
|
||||
services
|
||||
.AddDbContext<ProjectTemplateDbContext>(options =>
|
||||
{
|
||||
options
|
||||
.ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
||||
.UseInMemoryDatabase(_dbGuid.ToString())
|
||||
.EnableSensitiveDataLogging();
|
||||
});
|
||||
|
||||
var sp = services.BuildServiceProvider();
|
||||
Utilities.SetupDatabase<ProjectTemplateWebApplicationFactory<TStartup>>(sp);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Infrastructure
|
||||
{
|
||||
internal static class Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensure the database is created and seeded with both real and test data.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInitializer">The class that initialized the setup, for relevant logging.</typeparam>
|
||||
public static void SetupDatabase<TInitializer>(IServiceProvider serviceProvider)
|
||||
{
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger<TInitializer>();
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Database configuration started");
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<ProjectTemplateDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
(new TestDataSeeder(db, loggerFactory.CreateLogger<TestDataSeeder>())).SeedTestData();
|
||||
|
||||
logger.LogInformation("Database configuration completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"An error occurred setting up the database. Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously resolves a <see cref="ProjectTemplateDbContext"/> service scope and executes a delegate function.
|
||||
/// The scoped DbContext is disposed after the delegate is executed.
|
||||
/// </summary>
|
||||
/// <param name="func">The delegate <see cref="Func{T, TResult}"/> to execute with the scoped <see cref="ProjectTemplateDbContext"/></param>
|
||||
public static async Task<TResult> ExecuteWithDbScope<TResult>(this IServiceProvider provider, Func<ProjectTemplateDbContext, Task<TResult>> func)
|
||||
{
|
||||
using (IServiceScope scope = provider.CreateScope())
|
||||
{
|
||||
ProjectTemplateDbContext db = scope.ServiceProvider.GetRequiredService<ProjectTemplateDbContext>();
|
||||
return await func.Invoke(db).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a <see cref="ProjectTemplateDbContext"/> service scope and executes a delegate action.
|
||||
/// The scoped DbContext is disposed after the delegate is executed.
|
||||
/// </summary>
|
||||
/// <param name="action">The delegate <see cref="Action"/> to execute with the scoped <see cref="ProjectTemplateDbContext"/></param>
|
||||
public static void ExecuteWithDbScope(this IServiceProvider provider, Action<ProjectTemplateDbContext> action)
|
||||
{
|
||||
using (IServiceScope scope = provider.CreateScope())
|
||||
{
|
||||
ProjectTemplateDbContext db = scope.ServiceProvider.GetRequiredService<ProjectTemplateDbContext>();
|
||||
action.Invoke(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.DSX.ProjectTemplate.API;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// Each integration test is truly isolated because the test has private instances of:
|
||||
/// - <see cref="Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory{TEntryPoint}"/>
|
||||
/// - <see cref="Microsoft.AspNetCore.TestHost.TestServer"/>
|
||||
/// - <see cref="Microsoft.EntityFrameworkCore.InMemory"/> database
|
||||
/// </summary>
|
||||
[TestCategory("Integration")]
|
||||
[TestClass]
|
||||
public abstract class IntegrationTest : BaseTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets factory that creates <see cref="System.Net.Http.HttpClient"/> instances for sending HTTP requests to.
|
||||
/// </summary>
|
||||
protected WebApplicationFactory<TestStartup> Factory { get; }
|
||||
|
||||
protected IntegrationTest()
|
||||
{
|
||||
Factory = new CustomWebApplicationFactory<TestStartup>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ using System.Net.Http;
|
|||
using System.Threading.Tasks;
|
||||
|
||||
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests
|
||||
{
|
||||
public abstract class BaseTest
|
||||
{
|
|
@ -0,0 +1,64 @@
|
|||
using AutoMapper;
|
||||
using Microsoft.DSX.ProjectTemplate.API;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Abstractions;
|
||||
using Microsoft.DSX.ProjectTemplate.Test.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Each integration test is truly isolated because the test has private instances of:
|
||||
/// - <see cref="Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory{TEntryPoint}"/>
|
||||
/// - <see cref="Microsoft.AspNetCore.TestHost.TestServer"/>
|
||||
/// - <see cref="Microsoft.EntityFrameworkCore.InMemory"/> database
|
||||
/// </summary>
|
||||
[TestCategory("Integration")]
|
||||
[TestClass]
|
||||
public abstract class BaseIntegrationTest : BaseTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets factory that creates <see cref="System.Net.Http.HttpClient"/> instances for sending HTTP requests to.
|
||||
/// </summary>
|
||||
protected ProjectTemplateWebApplicationFactory<Startup> _factory;
|
||||
|
||||
protected IServiceProvider ServiceProvider { get; }
|
||||
|
||||
protected HttpClient Client { get; }
|
||||
|
||||
protected IMapper Mapper { get; }
|
||||
|
||||
protected BaseIntegrationTest()
|
||||
{
|
||||
_factory = new ProjectTemplateWebApplicationFactory<Startup>();
|
||||
ServiceProvider = _factory.Services;
|
||||
|
||||
Client = _factory.CreateClient();
|
||||
Client.DefaultRequestVersion = new Version(2, 0);
|
||||
|
||||
Mapper = _factory.Services.GetRequiredService<IMapper>();
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
Client.Dispose();
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a <see cref="IEmailService"/> service scope and gets the count of sent emails.
|
||||
/// </summary>
|
||||
/// <returns>Count of emails sent.</returns>
|
||||
protected int GetSentCount()
|
||||
{
|
||||
using (IServiceScope scope = ServiceProvider.CreateScope())
|
||||
{
|
||||
var emailService = (FakeEmailService)scope.ServiceProvider.GetRequiredService<IEmailService>();
|
||||
return emailService.GetSentCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Integration.Group
|
||||
{
|
||||
public abstract class BaseGroupTest : BaseIntegrationTest
|
||||
{
|
||||
protected GroupDto GetGroupDto()
|
||||
{
|
||||
return new GroupDto()
|
||||
{
|
||||
Name = RandomFactory.GetCompanyName(),
|
||||
IsActive = RandomFactory.GetBoolean()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.DSX.ProjectTemplate.Test.Infrastructure;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Integration.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Groups - Create")]
|
||||
public class CreateGroupTests : BaseGroupTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task CreateGroup_ValidDto_Success()
|
||||
{
|
||||
// Arrange
|
||||
var dto = GetGroupDto();
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<GroupDto>(response);
|
||||
result.Id.Should().BeGreaterThan(0);
|
||||
result.Name.Should().Be(dto.Name);
|
||||
result.IsActive.Should().Be(dto.IsActive);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[DataRow("\t")]
|
||||
public async Task CreateGroup_MissingName_BadRequest(string name)
|
||||
{
|
||||
// Arrange
|
||||
var dto = GetGroupDto();
|
||||
dto.Name = name;
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CreateGroup_NameTooLong_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var dto = GetGroupDto();
|
||||
dto.Name = RandomFactory.GetAlphanumericString(Constants.MaximumLengths.StringColumn + 1);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CreateGroup_NameAlreadyExists_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
Data.Models.Group randomGroup = null;
|
||||
ServiceProvider.ExecuteWithDbScope(db =>
|
||||
{
|
||||
randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
});
|
||||
var dto = GetGroupDto();
|
||||
dto.Name = randomGroup.Name;
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CreateGroup_ValidDto_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var dto = GetGroupDto();
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
GetSentCount().Should().Be(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Integration.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Groups - Delete")]
|
||||
public class DeleteGroupTests : BaseGroupTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow(5)]
|
||||
public async Task DeleteGroup_ValidId_Success(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.DeleteAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<bool>(response);
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(-1)]
|
||||
public async Task DeleteGroup_InvalidId_BadRequest(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.DeleteAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(int.MaxValue)]
|
||||
public async Task DeleteGroup_IdDoesNotExist_NotFound(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.DeleteAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Integration.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Groups - Get")]
|
||||
public class GetGroupTests : BaseGroupTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task GetAllGroup_Valid_Success()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/groups");
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<IEnumerable<GroupDto>>(response);
|
||||
result.Should().HaveCountGreaterThan(0);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
public async Task GetByIdGroup_ValidId_Success(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<GroupDto>(response);
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().Be(groupId);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(-1)]
|
||||
public async Task GetByIdGroup_InvalidId_BadRequest(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(int.MaxValue)]
|
||||
public async Task GetByIdGroup_IdDoesNotExist_NotFound(int groupId)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/api/groups/{groupId}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.DSX.ProjectTemplate.Test.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Integration.Group
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Groups - Update")]
|
||||
public class UpdateGroupTests : BaseGroupTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task UpdateGroup_ValidDto_Success()
|
||||
{
|
||||
// Arrange
|
||||
var dto = GetGroupDto();
|
||||
dto.Id = 4;
|
||||
|
||||
// Act
|
||||
var response = await Client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
var result = await EnsureObject<GroupDto>(response);
|
||||
result.Id.Should().Be(dto.Id);
|
||||
result.Name.Should().Be(dto.Name);
|
||||
result.IsActive.Should().Be(dto.IsActive);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(-1)]
|
||||
public async Task UpdateGroup_InvalidId_BadRequest(int id)
|
||||
{
|
||||
// Arrange
|
||||
Data.Models.Group randomGroup = null;
|
||||
ServiceProvider.ExecuteWithDbScope(db =>
|
||||
{
|
||||
randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
});
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
dto.Id = id;
|
||||
|
||||
// Act
|
||||
var response = await Client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(int.MaxValue)]
|
||||
public async Task UpdateGroup_IdDoesNotExist_NotFound(int id)
|
||||
{
|
||||
// Arrange
|
||||
Data.Models.Group randomGroup = null;
|
||||
ServiceProvider.ExecuteWithDbScope(db =>
|
||||
{
|
||||
randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
});
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
dto.Id = id;
|
||||
|
||||
// Act
|
||||
var response = await Client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[DataRow("\t")]
|
||||
public async Task UpdateGroup_MissingName_BadRequest(string name)
|
||||
{
|
||||
// Arrange
|
||||
Data.Models.Group randomGroup = null;
|
||||
ServiceProvider.ExecuteWithDbScope(db =>
|
||||
{
|
||||
randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
});
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
dto.Name = name;
|
||||
|
||||
// Act
|
||||
var response = await Client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task UpdateGroup_NameTooLong_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
Data.Models.Group randomGroup = null;
|
||||
ServiceProvider.ExecuteWithDbScope(db =>
|
||||
{
|
||||
randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
});
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
dto.Name = RandomFactory.GetAlphanumericString(Constants.MaximumLengths.StringColumn + 1);
|
||||
|
||||
// Act
|
||||
var response = await Client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task UpdateGroup_NameAlreadyExists_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
Data.Models.Group randomGroup = null;
|
||||
Data.Models.Group differentGroup = null;
|
||||
ServiceProvider.ExecuteWithDbScope(async db =>
|
||||
{
|
||||
randomGroup = SeedHelper.GetRandomGroup(db);
|
||||
differentGroup = await db.Groups
|
||||
.Where(x => x.Id != randomGroup.Id)
|
||||
.OrderBy(x => Guid.NewGuid())
|
||||
.FirstAsync();
|
||||
});
|
||||
var dto = Mapper.Map<GroupDto>(randomGroup);
|
||||
dto.Name = differentGroup.Name;
|
||||
|
||||
// Act
|
||||
var response = await Client.PutAsJsonAsync("/api/groups", dto);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,10 +16,10 @@ using System.Collections.Generic;
|
|||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Unit
|
||||
{
|
||||
[TestCategory("Unit")]
|
||||
public abstract class UnitTest : BaseTest
|
||||
public abstract class BaseUnitTest : BaseTest
|
||||
{
|
||||
protected ILoggerFactory LoggerFactory { get; set; }
|
||||
|
||||
|
@ -31,7 +31,7 @@ namespace Microsoft.DSX.ProjectTemplate.Test
|
|||
|
||||
protected Mock<IEmailService> MockEmailService { get; set; } = new Mock<IEmailService>();
|
||||
|
||||
protected UnitTest()
|
||||
protected BaseUnitTest()
|
||||
{
|
||||
// redirect all logging to console
|
||||
var services = new ServiceCollection();
|
|
@ -0,0 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Unit.DtoValidation
|
||||
{
|
||||
public abstract class BaseDtoTest : BaseUnitTest
|
||||
{
|
||||
protected ValidationResult FindMember(IEnumerable<ValidationResult> validationResults, string memberNameToFind)
|
||||
{
|
||||
return validationResults
|
||||
.FirstOrDefault(validationResult => validationResult.MemberNames.Any(
|
||||
memberName => memberName.Equals(memberNameToFind)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.DSX.ProjectTemplate.Data;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.DTOs;
|
||||
using Microsoft.DSX.ProjectTemplate.Data.Utilities;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Unit.DtoValidation
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("Group")]
|
||||
public class GroupDtoTests : BaseDtoTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void GroupDtoValidation_Valid_NoErrors()
|
||||
{
|
||||
var dto = new GroupDto
|
||||
{
|
||||
Name = RandomFactory.GetCompanyName(),
|
||||
};
|
||||
|
||||
var validationContext = new ValidationContext(dto);
|
||||
|
||||
var validationResults = dto.Validate(validationContext);
|
||||
|
||||
validationResults.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
public void GroupDtoValidation_MissingName_HasValidationErrors(string name)
|
||||
{
|
||||
var dto = new GroupDto
|
||||
{
|
||||
Name = name,
|
||||
};
|
||||
|
||||
var validationContext = new ValidationContext(dto);
|
||||
|
||||
var validationResults = dto.Validate(validationContext);
|
||||
|
||||
validationResults.Should().HaveCountGreaterThan(0);
|
||||
validationResults.FirstOrDefault(validationResult => validationResult.MemberNames.Any(memberName => memberName.Equals(nameof(GroupDto.Name)))).Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GroupDtoValidation_NameTooLong_HasValidationErrors()
|
||||
{
|
||||
var dto = new GroupDto
|
||||
{
|
||||
Name = RandomFactory.GetAlphanumericString(Constants.MaximumLengths.StringColumn + 1),
|
||||
};
|
||||
|
||||
var validationContext = new ValidationContext(dto);
|
||||
|
||||
var validationResults = dto.Validate(validationContext);
|
||||
|
||||
validationResults.Should().HaveCountGreaterThan(0);
|
||||
validationResults.FirstOrDefault(validationResult => validationResult.MemberNames.Any(memberName => memberName.Equals(nameof(GroupDto.Name)))).Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Infrastructure
|
||||
namespace Microsoft.DSX.ProjectTemplate.Test.Tests.Unit.Infrastructure
|
||||
{
|
||||
[TestClass]
|
||||
public class InfrastructureTests : UnitTest
|
||||
[TestCategory("Infrastructure")]
|
||||
public class InfrastructureTests : BaseUnitTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Infrastructure_AutoMapper_ConfigurationIsValid()
|
Загрузка…
Ссылка в новой задаче