From 4aac5eb1d0782a75f9164fc8550a9f6c0748cc33 Mon Sep 17 00:00:00 2001 From: midthunassetbridge Date: Sun, 8 Jun 2025 16:50:30 +0100 Subject: [PATCH 1/4] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 41ffa34..fa736c9 100644 --- a/.gitignore +++ b/.gitignore @@ -229,3 +229,4 @@ _Pvt_Extensions # FAKE - F# Make .fake/ +.angular/ From fc322b9071218fceb43db480c986de6fa7129965 Mon Sep 17 00:00:00 2001 From: midthunassetbridge Date: Sun, 8 Jun 2025 22:55:32 +0100 Subject: [PATCH 2/4] check in --- Controllers/CounterController.cs | 26 ++++++++++ Controllers/SanctionedEntitiesController.cs | 31 +++++++----- Models/SanctionedEntity.cs | 10 ---- Program.cs | 53 +++++++++++++++++++-- Services/DatabaseService.cs | 36 -------------- Services/IDatabaseService.cs | 13 ----- ajgre-technical-interview.csproj | 9 ++++ ajgre-technical-interview.sln | 36 ++++++++++++++ 8 files changed, 140 insertions(+), 74 deletions(-) create mode 100644 Controllers/CounterController.cs delete mode 100644 Models/SanctionedEntity.cs delete mode 100644 Services/DatabaseService.cs delete mode 100644 Services/IDatabaseService.cs diff --git a/Controllers/CounterController.cs b/Controllers/CounterController.cs new file mode 100644 index 0000000..ceb91f6 --- /dev/null +++ b/Controllers/CounterController.cs @@ -0,0 +1,26 @@ +using AJGRE.Application.DTOs; +using AJGRE.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace ajgre_technical_interview.Controllers +{ + public class CounterController : Controller + { + private readonly CounterService _service; + public CounterController(CounterService service) => _service = service; + + [HttpGet] + public async Task> GetCurrent() + { + var dto = await _service.GetCurrentAsync(); + return Ok(dto); + } + + [HttpPost("increment")] + public async Task> Increment() + { + var dto = await _service.IncrementAsync(); + return Ok(dto); + } + } +} diff --git a/Controllers/SanctionedEntitiesController.cs b/Controllers/SanctionedEntitiesController.cs index e6946c6..81de5ef 100644 --- a/Controllers/SanctionedEntitiesController.cs +++ b/Controllers/SanctionedEntitiesController.cs @@ -1,4 +1,5 @@ -using ajgre_technical_interview.Services; +using AJGRE.Application.DTOs; +using AJGRE.Application.Interfaces; using Microsoft.AspNetCore.Mvc; namespace ajgre_technical_interview.Controllers @@ -7,27 +8,33 @@ namespace ajgre_technical_interview.Controllers [Route("api/sanctioned-entities")] public class SanctionedEntitiesController : ControllerBase { - private readonly IDatabaseService _databaseService; - public SanctionedEntitiesController(IDatabaseService databaseService) + private readonly ISanctionedEntityService _service; + public SanctionedEntitiesController(ISanctionedEntityService service) => _service = service; + + [HttpGet] + public async Task>> ListAll() { - _databaseService = databaseService; + var list = await _service.ListAllAsync(); + return Ok(list); } - - [HttpGet] - public async Task GetSanctionedEntities() + [HttpPost] + public async Task Create([FromBody] EntityDto dto) { try { - var entities = await _databaseService.GetSanctionedEntitiesAsync(); - return Ok(entities); + await _service.AddAsync(dto); + return CreatedAtAction(nameof(ListAll), null); } - catch (Exception ex) + catch (ArgumentException ex) { - return Problem(ex.Message); + return BadRequest(ex.Message); + } + catch (InvalidOperationException ex) + { + return Conflict(ex.Message); } - } } } diff --git a/Models/SanctionedEntity.cs b/Models/SanctionedEntity.cs deleted file mode 100644 index ab343c5..0000000 --- a/Models/SanctionedEntity.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ajgre_technical_interview.Models -{ - public class SanctionedEntity - { - public Guid Id => Guid.NewGuid(); - public string Name { get; set; } = string.Empty; - public string Domicile { get; set; } = string.Empty; - public bool Accepted { get; set; } - } -} diff --git a/Program.cs b/Program.cs index c43b257..e4059fb 100644 --- a/Program.cs +++ b/Program.cs @@ -1,11 +1,35 @@ -using ajgre_technical_interview.Services; + + +using AJGRE.Application.Interfaces; +using AJGRE.Application.Services; +using AJGRE.Infrastructure.Configuration; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); -builder.Services.AddSingleton(); +builder.Services.Configure(options => +{ + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Status = StatusCodes.Status400BadRequest, + Title = "One or more validation errors occurred." + }; + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json" } + }; + }; +}); + +builder.Services.AddInfrastructure(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -15,7 +39,30 @@ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } - +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else +{ + app.UseExceptionHandler(errorApp => + { + errorApp.Run(async context => + { + var exceptionHandlerFeature = context.Features.Get(); + var error = exceptionHandlerFeature?.Error; + var problem = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Title = "An unexpected error occurred.", + Detail = error?.Message + }; + context.Response.StatusCode = problem.Status.Value; + context.Response.ContentType = "application/problem+json"; + await context.Response.WriteAsJsonAsync(problem); + }); + }); +} app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs deleted file mode 100644 index 330647d..0000000 --- a/Services/DatabaseService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using ajgre_technical_interview.Models; - -namespace ajgre_technical_interview.Services -{ - public class DatabaseService : IDatabaseService - { - private static readonly IList SanctionedEntities = new List - { - new SanctionedEntity { Name = "Forbidden Company", Domicile = "Mars", Accepted = false }, - new SanctionedEntity { Name = "Allowed Company", Domicile = "Venus", Accepted = true }, - new SanctionedEntity { Name = "Good Ltd", Domicile = "Saturn", Accepted = true }, - new SanctionedEntity { Name = "Evil Plc", Domicile = "Venus", Accepted = false } - }; - - public async Task> GetSanctionedEntitiesAsync() - { - var entities = SanctionedEntities - .OrderBy(e => e.Name) - .ThenBy(e => e.Domicile) - .ToList(); - - return await Task.FromResult(entities); - } - - public async Task GetSanctionedEntityByIdAsync(Guid id) - { - return await Task.FromResult(SanctionedEntities.First(e => e.Id.Equals(id))); - } - - public async Task CreateSanctionedEntityAsync(SanctionedEntity sanctionedEntity) - { - SanctionedEntities.Add(sanctionedEntity); - return await Task.FromResult(sanctionedEntity); - } - } -} \ No newline at end of file diff --git a/Services/IDatabaseService.cs b/Services/IDatabaseService.cs deleted file mode 100644 index 2dfff96..0000000 --- a/Services/IDatabaseService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ajgre_technical_interview.Models; - -namespace ajgre_technical_interview.Services -{ - public interface IDatabaseService - { - Task> GetSanctionedEntitiesAsync(); - - Task GetSanctionedEntityByIdAsync(Guid id); - - Task CreateSanctionedEntityAsync(SanctionedEntity sanctionedEntity); - } -} \ No newline at end of file diff --git a/ajgre-technical-interview.csproj b/ajgre-technical-interview.csproj index 55a2501..43135cd 100644 --- a/ajgre-technical-interview.csproj +++ b/ajgre-technical-interview.csproj @@ -22,6 +22,15 @@ + + + + + + + + + diff --git a/ajgre-technical-interview.sln b/ajgre-technical-interview.sln index 9eee50e..6706635 100644 --- a/ajgre-technical-interview.sln +++ b/ajgre-technical-interview.sln @@ -5,6 +5,18 @@ VisualStudioVersion = 17.5.33627.172 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ajgre-technical-interview", "ajgre-technical-interview.csproj", "{4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Domain", "..\AJRE.Domain\AJGRE.Domain.csproj", "{5B3E0263-51F6-4271-8222-AA6187CB0AD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Application", "..\AJGRE.Application\AJGRE.Application.csproj", "{A80B80A4-5858-44AB-A6E8-92731A18FAFF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Infrastructure", "..\AJGRE.Infrastructure\AJGRE.Infrastructure.csproj", "{00D2D57C-2495-4766-A38D-D8AB3571F10E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Application.Tests", "..\AJGRE.Application.Tests\AJGRE.Application.Tests.csproj", "{4235CF2A-C986-4C38-9397-5762F576172B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.WebApi.Tests", "..\AJGRE.WebApi.Tests\AJGRE.WebApi.Tests.csproj", "{51648979-BF80-41D2-9752-1BD97B1DE56B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,10 +27,34 @@ Global {4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}.Release|Any CPU.Build.0 = Release|Any CPU + {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Release|Any CPU.Build.0 = Release|Any CPU + {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Release|Any CPU.Build.0 = Release|Any CPU + {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Release|Any CPU.Build.0 = Release|Any CPU + {4235CF2A-C986-4C38-9397-5762F576172B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4235CF2A-C986-4C38-9397-5762F576172B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4235CF2A-C986-4C38-9397-5762F576172B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4235CF2A-C986-4C38-9397-5762F576172B}.Release|Any CPU.Build.0 = Release|Any CPU + {51648979-BF80-41D2-9752-1BD97B1DE56B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51648979-BF80-41D2-9752-1BD97B1DE56B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51648979-BF80-41D2-9752-1BD97B1DE56B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51648979-BF80-41D2-9752-1BD97B1DE56B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4235CF2A-C986-4C38-9397-5762F576172B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {51648979-BF80-41D2-9752-1BD97B1DE56B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CF39E593-2D0F-4B41-B320-443070DDD858} EndGlobalSection From 0bf8f08ce4f27e05b4a7703d11816e6bdaa7939e Mon Sep 17 00:00:00 2001 From: midthunassetbridge Date: Tue, 10 Jun 2025 21:39:15 +0100 Subject: [PATCH 3/4] check in files --- ClientApp/src/app/app.component.html | 4 + ClientApp/src/app/app.component.ts | 20 +++++ ClientApp/src/app/app.module.ts | 23 +++-- .../components/counter/counter.component.html | 2 +- .../counter/counter.component.spec.ts | 57 ++++++++----- .../components/counter/counter.component.ts | 7 +- .../app/components/error/error.component.css | 10 +++ .../app/components/error/error.component.html | 3 + .../app/components/error/error.component.ts | 17 ++++ .../jumbotron-counter.component.html | 2 +- .../jumbotron-counter.component.ts | 5 ++ .../nav-menu/nav-menu.component.html | 3 + .../components/spinner/spinner.component.css | 26 ++++++ .../components/spinner/spinner.component.html | 3 + .../components/spinner/spinner.component.ts | 17 ++++ .../app/components/toast/toast.component.css | 39 +++++++++ .../app/components/toast/toast.component.html | 20 +++++ .../app/components/toast/toast.component.ts | 29 +++++++ ClientApp/src/app/http.interceptor.ts | 37 +++++++++ ClientApp/src/app/models/counter-model.ts | 4 + .../add-sanctioned-entity.component.css | 0 .../add-sanctioned-entity.component.html | 40 +++++++++ .../add-sanctioned-entity.component.spec.ts | 83 +++++++++++++++++++ .../add-sanctioned-entity.component.ts | 41 +++++++++ .../sanctioned-entities.component.html | 0 .../sanctioned-entities.component.spec.ts | 39 +++++++++ .../sanctioned-entities.component.ts | 14 ++-- .../sanctioned-entities.resolver.ts | 14 ++++ .../sanctioned-entity-routing.module.ts | 21 +++++ .../sanctioned-entity.module.ts | 22 +++++ .../services/sanctioned-entities.service.ts | 27 ++++++ ClientApp/src/app/services/counter.service.ts | 17 ++++ ClientApp/src/app/services/error.service.ts | 15 ++++ .../services/sanctioned-entities.service.ts | 22 ----- ClientApp/src/app/services/spinner.service.ts | 17 ++++ ClientApp/src/app/services/toast.service.ts | 22 +++++ Controllers/CounterController.cs | 26 ------ Program.cs | 2 +- ajgre-technical-interview.csproj | 11 +++ 39 files changed, 676 insertions(+), 85 deletions(-) create mode 100644 ClientApp/src/app/components/error/error.component.css create mode 100644 ClientApp/src/app/components/error/error.component.html create mode 100644 ClientApp/src/app/components/error/error.component.ts create mode 100644 ClientApp/src/app/components/spinner/spinner.component.css create mode 100644 ClientApp/src/app/components/spinner/spinner.component.html create mode 100644 ClientApp/src/app/components/spinner/spinner.component.ts create mode 100644 ClientApp/src/app/components/toast/toast.component.css create mode 100644 ClientApp/src/app/components/toast/toast.component.html create mode 100644 ClientApp/src/app/components/toast/toast.component.ts create mode 100644 ClientApp/src/app/http.interceptor.ts create mode 100644 ClientApp/src/app/models/counter-model.ts create mode 100644 ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.css create mode 100644 ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.html create mode 100644 ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.spec.ts create mode 100644 ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.ts rename ClientApp/src/app/{ => sanctioned-entitiy}/components/sanctioned-entities/sanctioned-entities.component.html (100%) create mode 100644 ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.spec.ts rename ClientApp/src/app/{ => sanctioned-entitiy}/components/sanctioned-entities/sanctioned-entities.component.ts (57%) create mode 100644 ClientApp/src/app/sanctioned-entitiy/sanctioned-entities.resolver.ts create mode 100644 ClientApp/src/app/sanctioned-entitiy/sanctioned-entity-routing.module.ts create mode 100644 ClientApp/src/app/sanctioned-entitiy/sanctioned-entity.module.ts create mode 100644 ClientApp/src/app/sanctioned-entitiy/services/sanctioned-entities.service.ts create mode 100644 ClientApp/src/app/services/counter.service.ts create mode 100644 ClientApp/src/app/services/error.service.ts delete mode 100644 ClientApp/src/app/services/sanctioned-entities.service.ts create mode 100644 ClientApp/src/app/services/spinner.service.ts create mode 100644 ClientApp/src/app/services/toast.service.ts delete mode 100644 Controllers/CounterController.cs diff --git a/ClientApp/src/app/app.component.html b/ClientApp/src/app/app.component.html index 7173845..d51ea9b 100644 --- a/ClientApp/src/app/app.component.html +++ b/ClientApp/src/app/app.component.html @@ -1,6 +1,10 @@
+ + + +
diff --git a/ClientApp/src/app/app.component.ts b/ClientApp/src/app/app.component.ts index 0a40b8c..8b082e5 100644 --- a/ClientApp/src/app/app.component.ts +++ b/ClientApp/src/app/app.component.ts @@ -1,4 +1,7 @@ import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ErrorService } from './services/error.service'; +import { NavigationStart, Router } from '@angular/router'; @Component({ selector: 'app-root', @@ -6,4 +9,21 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'app'; + errorMessage$: Observable; + + constructor( + + private errorSvc: ErrorService, + private router: Router + ) { + + this.errorMessage$ = this.errorSvc.errors$; + + + this.router.events.subscribe(evt => { + if (evt instanceof NavigationStart) { + this.errorSvc.clear(); + } + }); + } } diff --git a/ClientApp/src/app/app.module.ts b/ClientApp/src/app/app.module.ts index f4fdf93..b940416 100644 --- a/ClientApp/src/app/app.module.ts +++ b/ClientApp/src/app/app.module.ts @@ -1,15 +1,19 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { HttpClientModule } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { NavMenuComponent } from './components/nav-menu/nav-menu.component'; import { HomeComponent } from './components/home/home.component'; import { CounterComponent } from './components/counter/counter.component'; -import { SanctionedEntitiesComponent } from './components/sanctioned-entities/sanctioned-entities.component'; + import { JumbotronCounterComponent } from './components/jumbotron-counter/jumbotron-counter.component'; +import { SpinnerComponent } from './components/spinner/spinner.component'; +import { AppHttpInterceptor } from './http.interceptor'; +import { ErrorComponent } from './components/error/error.component'; +import { ToastComponent } from './components/toast/toast.component'; @NgModule({ @@ -18,8 +22,10 @@ import { JumbotronCounterComponent } from './components/jumbotron-counter/jumbot NavMenuComponent, HomeComponent, CounterComponent, - SanctionedEntitiesComponent, - JumbotronCounterComponent + ToastComponent, + JumbotronCounterComponent, + SpinnerComponent, + ErrorComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), @@ -28,10 +34,15 @@ import { JumbotronCounterComponent } from './components/jumbotron-counter/jumbot RouterModule.forRoot([ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'counter', component: CounterComponent }, - { path: 'sanctioned-entities', component: SanctionedEntitiesComponent }, + { path: 'sanctioned-entities', + loadChildren: () => import('./sanctioned-entitiy/sanctioned-entity.module') + .then(m => m.SanctionedEntitiesModule) }, ]) ], - providers: [], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: AppHttpInterceptor, multi: true } + + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/ClientApp/src/app/components/counter/counter.component.html b/ClientApp/src/app/components/counter/counter.component.html index 89b9c80..364095a 100644 --- a/ClientApp/src/app/components/counter/counter.component.html +++ b/ClientApp/src/app/components/counter/counter.component.html @@ -2,6 +2,6 @@

Counter

This is a simple example of an Angular component.

-

Current count: {{ currentCount }}

+

Current count: {{ count$ | async }}

diff --git a/ClientApp/src/app/components/counter/counter.component.spec.ts b/ClientApp/src/app/components/counter/counter.component.spec.ts index 09b3336..df5801f 100644 --- a/ClientApp/src/app/components/counter/counter.component.spec.ts +++ b/ClientApp/src/app/components/counter/counter.component.spec.ts @@ -1,36 +1,51 @@ +// src/app/counter/counter.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { CounterComponent } from './counter.component'; -describe('CounterComponent', () => { - let component: CounterComponent; +import { BehaviorSubject, of, tap } from 'rxjs'; +import { CounterService } from 'src/app/services/counter.service'; +describe('CounterComponent (stubbed service)', () => { let fixture: ComponentFixture; + let component: CounterComponent; + let counterSpy: jasmine.SpyObj; + let value$: BehaviorSubject; - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ CounterComponent ] - }) - .compileComponents(); - }); + beforeEach(async () => { + // New subject for each spec: + value$ = new BehaviorSubject(0); + counterSpy = jasmine.createSpyObj( + 'CounterService', + ['increment'], + { value$ } + ); + + await TestBed.configureTestingModule({ + declarations: [CounterComponent], + providers: [ + { provide: CounterService, useValue: counterSpy } + ] + }).compileComponents(); - beforeEach(() => { fixture = TestBed.createComponent(CounterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should display a title', () => { - const titleText = fixture.nativeElement.querySelector('h1').textContent; - expect(titleText).toEqual('Counter'); - }); + it('should start with 0, then increment to 1 when clicked', () => { + let displayed!: number; + component.count$.subscribe(v => displayed = v); - it('should start with count 0, then increments by 1 when clicked', () => { - const countElement = fixture.nativeElement.querySelector('strong'); - expect(countElement.textContent).toEqual('0'); + // Initial should be zero + expect(displayed).toBe(0); - const incrementButton = fixture.nativeElement.querySelector('button'); - incrementButton.click(); - fixture.detectChanges(); - expect(countElement.textContent).toEqual('1'); + // Stub increment() to push 1 into the subject + counterSpy.increment.and.callFake(() => { + value$.next(1); + return of(1); + }); + + component.incrementCounter(); // calls stubbed increment() + expect(counterSpy.increment).toHaveBeenCalled(); + expect(displayed).toBe(1); }); }); diff --git a/ClientApp/src/app/components/counter/counter.component.ts b/ClientApp/src/app/components/counter/counter.component.ts index 1f336aa..77331b7 100644 --- a/ClientApp/src/app/components/counter/counter.component.ts +++ b/ClientApp/src/app/components/counter/counter.component.ts @@ -1,13 +1,16 @@ import { Component } from '@angular/core'; +import { CounterService } from '../../services/counter.service'; @Component({ selector: 'app-counter-component', templateUrl: './counter.component.html' }) export class CounterComponent { - public currentCount = 0; + count$ = this.counter.value$; + constructor(private counter: CounterService) { + } public incrementCounter() { - this.currentCount++; + this.counter.increment(); } } diff --git a/ClientApp/src/app/components/error/error.component.css b/ClientApp/src/app/components/error/error.component.css new file mode 100644 index 0000000..bee922d --- /dev/null +++ b/ClientApp/src/app/components/error/error.component.css @@ -0,0 +1,10 @@ +.error-message { + + width: 100%; + background-color: #f8d7da; + color: #842029; + padding: 0.75rem; + text-align: center; + z-index: 1001; + font-weight: 500; +} \ No newline at end of file diff --git a/ClientApp/src/app/components/error/error.component.html b/ClientApp/src/app/components/error/error.component.html new file mode 100644 index 0000000..771c9b0 --- /dev/null +++ b/ClientApp/src/app/components/error/error.component.html @@ -0,0 +1,3 @@ +
+ {{ msg }} +
\ No newline at end of file diff --git a/ClientApp/src/app/components/error/error.component.ts b/ClientApp/src/app/components/error/error.component.ts new file mode 100644 index 0000000..b4b8569 --- /dev/null +++ b/ClientApp/src/app/components/error/error.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { ErrorService } from 'src/app/services/error.service'; + +@Component({ + selector: 'app-error', + templateUrl: './error.component.html', + styleUrls: ['./error.component.css'] +}) +export class ErrorComponent { + errorMessage$: Observable; + + constructor(private errorService: ErrorService) { + this.errorMessage$ = this.errorService.errors$; + } +} \ No newline at end of file diff --git a/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.html b/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.html index 4d39017..d981ec1 100644 --- a/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.html +++ b/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.html @@ -1,4 +1,4 @@
-

Current count:

+

Current count:{{count$ |async}}

Please include the counter here

diff --git a/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.ts b/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.ts index 6884723..5966e88 100644 --- a/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.ts +++ b/ClientApp/src/app/components/jumbotron-counter/jumbotron-counter.component.ts @@ -1,8 +1,13 @@ import { Component } from '@angular/core'; +import { CounterService } from '../../services/counter.service'; @Component({ selector: 'app-jumbotron-counter', templateUrl: './jumbotron-counter.component.html' }) export class JumbotronCounterComponent { + count$ = this.counter.value$; + constructor(private counter: CounterService) { + + } } diff --git a/ClientApp/src/app/components/nav-menu/nav-menu.component.html b/ClientApp/src/app/components/nav-menu/nav-menu.component.html index 6ca2d59..7b5830d 100644 --- a/ClientApp/src/app/components/nav-menu/nav-menu.component.html +++ b/ClientApp/src/app/components/nav-menu/nav-menu.component.html @@ -33,6 +33,9 @@ + diff --git a/ClientApp/src/app/components/spinner/spinner.component.css b/ClientApp/src/app/components/spinner/spinner.component.css new file mode 100644 index 0000000..0c89fe0 --- /dev/null +++ b/ClientApp/src/app/components/spinner/spinner.component.css @@ -0,0 +1,26 @@ +.spinner-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.spinner { + width: 50px; + height: 50px; + border: 5px solid #ccc; + border-top-color: #333; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/ClientApp/src/app/components/spinner/spinner.component.html b/ClientApp/src/app/components/spinner/spinner.component.html new file mode 100644 index 0000000..b30200f --- /dev/null +++ b/ClientApp/src/app/components/spinner/spinner.component.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/ClientApp/src/app/components/spinner/spinner.component.ts b/ClientApp/src/app/components/spinner/spinner.component.ts new file mode 100644 index 0000000..37f9c52 --- /dev/null +++ b/ClientApp/src/app/components/spinner/spinner.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { SpinnerService } from 'src/app/services/spinner.service'; + +@Component({ + selector: 'app-spinner', + templateUrl: './spinner.component.html', + styleUrls: ['./spinner.component.css'] +}) +export class SpinnerComponent { + loading$: Observable; + + constructor(private spinner: SpinnerService) { + this.loading$ = this.spinner.loading$; + } +} \ No newline at end of file diff --git a/ClientApp/src/app/components/toast/toast.component.css b/ClientApp/src/app/components/toast/toast.component.css new file mode 100644 index 0000000..5517820 --- /dev/null +++ b/ClientApp/src/app/components/toast/toast.component.css @@ -0,0 +1,39 @@ +.toast-container { + position: fixed; + top: 50%; + right: 1rem; + transform: translateY(-50%); + z-index: 1200; + pointer-events: none; /* allow clicks to pass through */ +} + +.toast { + pointer-events: auto; + border-radius: 0.25rem; + /* slide in from right, then slide back out */ + animation: + slideIn 0.3s ease-out forwards, + slideOut 0.3s ease-in 2.7s forwards; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideOut { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(100%); + } +} diff --git a/ClientApp/src/app/components/toast/toast.component.html b/ClientApp/src/app/components/toast/toast.component.html new file mode 100644 index 0000000..50aab82 --- /dev/null +++ b/ClientApp/src/app/components/toast/toast.component.html @@ -0,0 +1,20 @@ + +
+ +
+
diff --git a/ClientApp/src/app/components/toast/toast.component.ts b/ClientApp/src/app/components/toast/toast.component.ts new file mode 100644 index 0000000..79894c5 --- /dev/null +++ b/ClientApp/src/app/components/toast/toast.component.ts @@ -0,0 +1,29 @@ +// src/app/toast/toast.component.ts +import { Component, OnInit, OnDestroy } from '@angular/core'; + +import { Subscription } from 'rxjs'; +import { ToastMessage, ToastService } from 'src/app/services/toast.service'; + +@Component({ + selector: 'app-toast', + templateUrl: './toast.component.html', + styleUrls: ['./toast.component.css'], +}) +export class ToastComponent implements OnInit, OnDestroy { + currentMsg: ToastMessage | null = null; + private sub!: Subscription; + + constructor(private toast: ToastService) {} + + ngOnInit() { + this.sub = this.toast.messages$.subscribe(msg => { + this.currentMsg = msg; + // auto-dismiss after 3s + setTimeout(() => this.currentMsg = null, 3000); + }); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } +} diff --git a/ClientApp/src/app/http.interceptor.ts b/ClientApp/src/app/http.interceptor.ts new file mode 100644 index 0000000..9c7feae --- /dev/null +++ b/ClientApp/src/app/http.interceptor.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { + HttpInterceptor, HttpRequest, HttpHandler, + HttpEvent, HttpErrorResponse +} from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { finalize, catchError } from 'rxjs/operators'; +import { SpinnerService } from './services/spinner.service'; +import { ErrorService } from './services/error.service'; + + +@Injectable() +export class AppHttpInterceptor implements HttpInterceptor { + constructor( + private spinner: SpinnerService, + private errorService: ErrorService + ) {} + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + // Show spinner on request start + this.spinner.show(); + console.log('HTTP Request started:', req.url); + return next.handle(req).pipe( + // Catch and report errors + catchError((err: HttpErrorResponse) => { + const msg = err.error?.message || err.statusText || 'Server Error'; + this.errorService.report(msg); + return throwError(() => err); + }), + // Hide spinner on both success and error + finalize(() => this.spinner.hide()) + ); + } +} \ No newline at end of file diff --git a/ClientApp/src/app/models/counter-model.ts b/ClientApp/src/app/models/counter-model.ts new file mode 100644 index 0000000..cf09607 --- /dev/null +++ b/ClientApp/src/app/models/counter-model.ts @@ -0,0 +1,4 @@ +// src/app/counter.model.ts +export interface CounterModel { + value: number; +} diff --git a/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.css b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.css new file mode 100644 index 0000000..e69de29 diff --git a/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.html b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.html new file mode 100644 index 0000000..702c4b4 --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.html @@ -0,0 +1,40 @@ +
+
+

Add Sanctioned Entity

+
+
+ + +
+ Name is required. +
+
+ +
+ + +
+ Domicile is required. +
+
+ +
+ An entity with this Name and Domicile already exists. +
+ +
+ + +
+ + + +
+ {{ error }} +
+
+
+
\ No newline at end of file diff --git a/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.spec.ts b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.spec.ts new file mode 100644 index 0000000..e36a8af --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.spec.ts @@ -0,0 +1,83 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { of, throwError } from 'rxjs'; +import { AddSanctionedEntityComponent } from './add-sanctioned-entity.component'; +import { SanctionedEntitiesService } from '../../services/sanctioned-entities.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { SanctionedEntity } from 'src/app/models/sanctioned-entity'; + +describe('AddSanctionedEntityComponent', () => { + let component: AddSanctionedEntityComponent; + let fixture: ComponentFixture; + let entitySrv: jasmine.SpyObj; + let toastSrv: jasmine.SpyObj; + + beforeEach(async () => { + const entitySpy = jasmine.createSpyObj('SanctionedEntitiesService', ['createSanctionedEntity']); + const toastSpy = jasmine.createSpyObj('ToastService', ['success', 'error']); + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + declarations: [AddSanctionedEntityComponent], + providers: [ + FormBuilder, + { provide: SanctionedEntitiesService, useValue: entitySpy }, + { provide: ToastService, useValue: toastSpy } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(AddSanctionedEntityComponent); + component = fixture.componentInstance; + + entitySrv = TestBed.inject( + SanctionedEntitiesService + ) as jasmine.SpyObj; + toastSrv = TestBed.inject( + ToastService + ) as jasmine.SpyObj; + + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + + + it('should call service and show success toast on valid submit', fakeAsync(() => { + const dto: SanctionedEntity = { + + name: 'A', + domicile: 'B', + accepted: true + } as any; + + + entitySrv.createSanctionedEntity.and.returnValue(of(dto)); + + + component.form.setValue({ name: 'A', domicile: 'B', accepted: true }); + component.submit(); + tick(); + expect(entitySrv.createSanctionedEntity).toHaveBeenCalledWith(dto); + expect(toastSrv.success).toHaveBeenCalledWith('Sanctioned Entity added successfully!'); + expect(component.error).toBe(''); + + expect(component.form.value.accepted).toBeFalse(); + })); + + it('should call error toast on service error', fakeAsync(() => { + const dto: SanctionedEntity = { id: '', name: 'X', domicile: 'Y', accepted: false } as any; + const serverErr = { error: { message: 'Dup!' } }; + + entitySrv.createSanctionedEntity.and.returnValue(throwError(() => serverErr)); + + component.form.setValue({ name: 'X', domicile: 'Y', accepted: false }); + component.submit(); + tick(); + + expect(entitySrv.createSanctionedEntity).toHaveBeenCalled(); + expect(toastSrv.error).toHaveBeenCalledWith('Dup!'); + })); +}); diff --git a/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.ts b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.ts new file mode 100644 index 0000000..0421101 --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/components/add-sanctioned-entity/add-sanctioned-entity.component.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { SanctionedEntity } from 'src/app/models/sanctioned-entity'; +import { SanctionedEntitiesService } from '../../services/sanctioned-entities.service'; +import { ToastService } from 'src/app/services/toast.service'; + + +@Component({ + selector: 'app-add-entity', + templateUrl: './add-sanctioned-entity.component.html', + styleUrls: ['./add-sanctioned-entity.component.css'] +}) +export class AddSanctionedEntityComponent { + form = this.fb.group({ + name: ['', Validators.required], + domicile: ['', Validators.required], + accepted: [false] + }); + error = ''; + + constructor(private fb: FormBuilder, private entitySrv: SanctionedEntitiesService, private toast: ToastService ) {} + + submit() { + if (this.form.invalid) + return; + + const dto = this.form.value as SanctionedEntity; + this.entitySrv.createSanctionedEntity(dto) + .subscribe({ + next: () => { + this.toast.success('Sanctioned Entity added successfully!'); + this.form.reset({ accepted: false }); + }, + + error: err => { + this.toast.error(err.error?.message || 'Failed to add entity'); + } + }); + } +} \ No newline at end of file diff --git a/ClientApp/src/app/components/sanctioned-entities/sanctioned-entities.component.html b/ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.html similarity index 100% rename from ClientApp/src/app/components/sanctioned-entities/sanctioned-entities.component.html rename to ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.html diff --git a/ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.spec.ts b/ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.spec.ts new file mode 100644 index 0000000..6a28a91 --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { SanctionedEntitiesComponent } from './sanctioned-entities.component'; +import { SanctionedEntity } from '../../../models/sanctioned-entity'; + +describe('SanctionedEntitiesComponent', () => { + let component: SanctionedEntitiesComponent; + let fixture: ComponentFixture; + const mockEntities: SanctionedEntity[] = [ + { id: '11111111-1111-1111-1111-111111111111', name: 'Entity A', domicile: 'X', accepted: true }, + { id: '22222222-2222-2222-2222-222222222222', name: 'Entity B', domicile: 'Y', accepted: false } + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SanctionedEntitiesComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { snapshot: { data: { entities: mockEntities } } } + } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SanctionedEntitiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should load resolved entities into `entities`', () => { + expect(component.entities).toEqual(mockEntities); + }); +}); \ No newline at end of file diff --git a/ClientApp/src/app/components/sanctioned-entities/sanctioned-entities.component.ts b/ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.ts similarity index 57% rename from ClientApp/src/app/components/sanctioned-entities/sanctioned-entities.component.ts rename to ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.ts index ca699d1..bc330fa 100644 --- a/ClientApp/src/app/components/sanctioned-entities/sanctioned-entities.component.ts +++ b/ClientApp/src/app/sanctioned-entitiy/components/sanctioned-entities/sanctioned-entities.component.ts @@ -1,6 +1,8 @@ import { Component } from '@angular/core'; -import { SanctionedEntity } from '../../models/sanctioned-entity'; +import { SanctionedEntity } from '../../../models/sanctioned-entity'; import { SanctionedEntitiesService } from '../../services/sanctioned-entities.service'; +import { ActivatedRoute } from '@angular/router'; + @Component({ selector: 'app-sanctioned-entities', @@ -9,9 +11,11 @@ import { SanctionedEntitiesService } from '../../services/sanctioned-entities.se export class SanctionedEntitiesComponent { public entities: SanctionedEntity[] = []; - constructor(private entitiesService: SanctionedEntitiesService) { - entitiesService.getSanctionedEntities().subscribe(entities => { - this.entities = entities; - }); + constructor(private route: ActivatedRoute) { + + } + + ngOnInit() { + this.entities = this.route.snapshot.data['entities']; } } diff --git a/ClientApp/src/app/sanctioned-entitiy/sanctioned-entities.resolver.ts b/ClientApp/src/app/sanctioned-entitiy/sanctioned-entities.resolver.ts new file mode 100644 index 0000000..3afda9a --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/sanctioned-entities.resolver.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { Observable } from 'rxjs'; +import { SanctionedEntity } from '../models/sanctioned-entity'; +import { SanctionedEntitiesService } from './services/sanctioned-entities.service'; + +@Injectable({ providedIn: 'root' }) +export class SanctionedEntitiesResolver implements Resolve { + constructor(private service: SanctionedEntitiesService) {} + + resolve(): Observable { + return this.service.getSanctionedEntities(); + } +} \ No newline at end of file diff --git a/ClientApp/src/app/sanctioned-entitiy/sanctioned-entity-routing.module.ts b/ClientApp/src/app/sanctioned-entitiy/sanctioned-entity-routing.module.ts new file mode 100644 index 0000000..9269029 --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/sanctioned-entity-routing.module.ts @@ -0,0 +1,21 @@ + + + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { SanctionedEntitiesComponent } from './components/sanctioned-entities/sanctioned-entities.component'; +import { AddSanctionedEntityComponent } from './components/add-sanctioned-entity/add-sanctioned-entity.component'; +import { SanctionedEntitiesResolver } from './sanctioned-entities.resolver'; + + +const routes: Routes = [ + { path: '', component: SanctionedEntitiesComponent,resolve: { entities: SanctionedEntitiesResolver } }, + { path: 'add', component: AddSanctionedEntityComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class SanctionedEntitiesRoutingModule {} + diff --git a/ClientApp/src/app/sanctioned-entitiy/sanctioned-entity.module.ts b/ClientApp/src/app/sanctioned-entitiy/sanctioned-entity.module.ts new file mode 100644 index 0000000..f3051f4 --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/sanctioned-entity.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { SanctionedEntitiesComponent } from './components/sanctioned-entities/sanctioned-entities.component'; +import { AddSanctionedEntityComponent } from './components/add-sanctioned-entity/add-sanctioned-entity.component'; +import { SanctionedEntitiesRoutingModule } from './sanctioned-entity-routing.module'; +import { SanctionedEntitiesService } from './services/sanctioned-entities.service'; + + +@NgModule({ + declarations: [ + SanctionedEntitiesComponent, + AddSanctionedEntityComponent + ], + imports: [ + CommonModule, + ReactiveFormsModule, + SanctionedEntitiesRoutingModule + ], + providers: [SanctionedEntitiesService], +}) +export class SanctionedEntitiesModule {} \ No newline at end of file diff --git a/ClientApp/src/app/sanctioned-entitiy/services/sanctioned-entities.service.ts b/ClientApp/src/app/sanctioned-entitiy/services/sanctioned-entities.service.ts new file mode 100644 index 0000000..576dce6 --- /dev/null +++ b/ClientApp/src/app/sanctioned-entitiy/services/sanctioned-entities.service.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SanctionedEntity } from 'src/app/models/sanctioned-entity'; + +@Injectable({ providedIn: 'root' }) +export class SanctionedEntitiesService { + private readonly apiUrl: string; + private readonly path = 'sanctioned-entities'; + + constructor( + private http: HttpClient, + @Inject('BASE_URL') baseUrl: string + ) { + this.apiUrl = baseUrl + 'api/'; + } + + public getSanctionedEntities(): Observable { + const url = `${this.apiUrl}${this.path}`; + return this.http.get(url); + } + + public createSanctionedEntity(entity: SanctionedEntity): Observable { + const url = `${this.apiUrl}${this.path}`; + return this.http.post(url, entity); + } +} \ No newline at end of file diff --git a/ClientApp/src/app/services/counter.service.ts b/ClientApp/src/app/services/counter.service.ts new file mode 100644 index 0000000..e41eeb5 --- /dev/null +++ b/ClientApp/src/app/services/counter.service.ts @@ -0,0 +1,17 @@ +// src/app/counter.service.ts +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class CounterService { + + private _value = new BehaviorSubject(0); + + readonly value$: Observable = this._value.asObservable(); + + + increment(): void { + const next = this._value.value + 1; + this._value.next(next); + } +} diff --git a/ClientApp/src/app/services/error.service.ts b/ClientApp/src/app/services/error.service.ts new file mode 100644 index 0000000..6dc8d66 --- /dev/null +++ b/ClientApp/src/app/services/error.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { Subject, Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class ErrorService { + private _errors = new Subject(); + readonly errors$: Observable = this._errors.asObservable(); + + report(message: string): void { + this._errors.next(message); + } + clear(): void { + this._errors.next(''); + } +} \ No newline at end of file diff --git a/ClientApp/src/app/services/sanctioned-entities.service.ts b/ClientApp/src/app/services/sanctioned-entities.service.ts deleted file mode 100644 index 1a90e11..0000000 --- a/ClientApp/src/app/services/sanctioned-entities.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { SanctionedEntity } from '../models/sanctioned-entity'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class SanctionedEntitiesService { - - private readonly apiUrl: string; - private readonly path = 'sanctioned-entities'; - - constructor(private http: HttpClient, @Inject('BASE_URL') baseUrl: string) { - this.apiUrl = baseUrl + 'api/'; - } - - public getSanctionedEntities(): Observable { - const url = this.apiUrl + this.path; - return this.http.get(url); - } -} diff --git a/ClientApp/src/app/services/spinner.service.ts b/ClientApp/src/app/services/spinner.service.ts new file mode 100644 index 0000000..f38d88e --- /dev/null +++ b/ClientApp/src/app/services/spinner.service.ts @@ -0,0 +1,17 @@ +// src/app/spinner.service.ts +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class SpinnerService { + private _loading = new BehaviorSubject(false); + readonly loading$: Observable = this._loading.asObservable(); + + show(): void { + this._loading.next(true); + } + + hide(): void { + this._loading.next(false); + } +} diff --git a/ClientApp/src/app/services/toast.service.ts b/ClientApp/src/app/services/toast.service.ts new file mode 100644 index 0000000..40d03c5 --- /dev/null +++ b/ClientApp/src/app/services/toast.service.ts @@ -0,0 +1,22 @@ +// src/app/toast.service.ts +import { Injectable } from '@angular/core'; +import { Subject, Observable } from 'rxjs'; + +export interface ToastMessage { + type: 'success' | 'error'; + text: string; +} + +@Injectable({ providedIn: 'root' }) +export class ToastService { + private _messages = new Subject(); + readonly messages$: Observable = this._messages.asObservable(); + + success(text: string) { + this._messages.next({ type: 'success', text }); + } + + error(text: string) { + this._messages.next({ type: 'error', text }); + } +} diff --git a/Controllers/CounterController.cs b/Controllers/CounterController.cs deleted file mode 100644 index ceb91f6..0000000 --- a/Controllers/CounterController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AJGRE.Application.DTOs; -using AJGRE.Application.Services; -using Microsoft.AspNetCore.Mvc; - -namespace ajgre_technical_interview.Controllers -{ - public class CounterController : Controller - { - private readonly CounterService _service; - public CounterController(CounterService service) => _service = service; - - [HttpGet] - public async Task> GetCurrent() - { - var dto = await _service.GetCurrentAsync(); - return Ok(dto); - } - - [HttpPost("increment")] - public async Task> Increment() - { - var dto = await _service.IncrementAsync(); - return Ok(dto); - } - } -} diff --git a/Program.cs b/Program.cs index e4059fb..4eb6f1b 100644 --- a/Program.cs +++ b/Program.cs @@ -28,7 +28,7 @@ }); builder.Services.AddInfrastructure(); -builder.Services.AddScoped(); + builder.Services.AddScoped(); var app = builder.Build(); diff --git a/ajgre-technical-interview.csproj b/ajgre-technical-interview.csproj index 43135cd..242693f 100644 --- a/ajgre-technical-interview.csproj +++ b/ajgre-technical-interview.csproj @@ -23,6 +23,12 @@ + + + + + + @@ -31,6 +37,11 @@ + + + + + From 2a98f73ebede78d74e9b40e00084e90e027a5fc8 Mon Sep 17 00:00:00 2001 From: midthunassetbridge Date: Tue, 10 Jun 2025 22:18:06 +0100 Subject: [PATCH 4/4] midthun --- .../AJGRE.Application.Tests.csproj | 30 ++++++++ .../Services/SanctionedEntityServiceTests.cs | 73 +++++++++++++++++++ AJGRE.Application.Tests/Usings.cs | 1 + AJGRE.Application/AJGRE.Application.csproj | 13 ++++ AJGRE.Application/DTOs/EntityDto.cs | 15 ++++ .../Interfaces/ISanctionedEntityService.cs | 16 ++++ .../Services/SanctionedEntityService.cs | 39 ++++++++++ .../AJGRE.Infrastructure.csproj | 17 +++++ .../Configuration/DependencyInjection.cs | 22 ++++++ .../Repository/SanctionedEntityRepository.cs | 51 +++++++++++++ AJGRE.WebApi.Tests/AJGRE.WebApi.Tests.csproj | 32 ++++++++ .../SanctionedEntitiesControllerTests.cs | 30 ++++++++ AJGRE.WebApi.Tests/Usings.cs | 1 + AJRE.Domain/AJGRE.Domain.csproj | 9 +++ AJRE.Domain/Entities/SanctionedEntity.cs | 12 +++ .../Interfaces/ISanctionedEntityRepository.cs | 17 +++++ ajgre-technical-interview.csproj | 24 +++++- ajgre-technical-interview.sln | 56 +++++++------- 18 files changed, 428 insertions(+), 30 deletions(-) create mode 100644 AJGRE.Application.Tests/AJGRE.Application.Tests.csproj create mode 100644 AJGRE.Application.Tests/Services/SanctionedEntityServiceTests.cs create mode 100644 AJGRE.Application.Tests/Usings.cs create mode 100644 AJGRE.Application/AJGRE.Application.csproj create mode 100644 AJGRE.Application/DTOs/EntityDto.cs create mode 100644 AJGRE.Application/Interfaces/ISanctionedEntityService.cs create mode 100644 AJGRE.Application/Services/SanctionedEntityService.cs create mode 100644 AJGRE.Infrastructure/AJGRE.Infrastructure.csproj create mode 100644 AJGRE.Infrastructure/Configuration/DependencyInjection.cs create mode 100644 AJGRE.Infrastructure/Repository/SanctionedEntityRepository.cs create mode 100644 AJGRE.WebApi.Tests/AJGRE.WebApi.Tests.csproj create mode 100644 AJGRE.WebApi.Tests/Controllers/SanctionedEntitiesControllerTests.cs create mode 100644 AJGRE.WebApi.Tests/Usings.cs create mode 100644 AJRE.Domain/AJGRE.Domain.csproj create mode 100644 AJRE.Domain/Entities/SanctionedEntity.cs create mode 100644 AJRE.Domain/Interfaces/ISanctionedEntityRepository.cs diff --git a/AJGRE.Application.Tests/AJGRE.Application.Tests.csproj b/AJGRE.Application.Tests/AJGRE.Application.Tests.csproj new file mode 100644 index 0000000..ad7d2e5 --- /dev/null +++ b/AJGRE.Application.Tests/AJGRE.Application.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/AJGRE.Application.Tests/Services/SanctionedEntityServiceTests.cs b/AJGRE.Application.Tests/Services/SanctionedEntityServiceTests.cs new file mode 100644 index 0000000..4ddc809 --- /dev/null +++ b/AJGRE.Application.Tests/Services/SanctionedEntityServiceTests.cs @@ -0,0 +1,73 @@ +using AJGRE.Application.DTOs; +using AJGRE.Application.Services; +using ajgre_technical_interview.Models; +using AJRE.Domain.Interfaces; +using FluentAssertions; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace AJGRE.Application.Tests.Services +{ + public class SanctionedEntityServiceTests + { + [Fact] + public async Task ListAllAsync_Maps_Entities_To_EntityDto() + { + var entities = new List + { + new SanctionedEntity {Name = "A", Domicile = "X", Accepted = true }, + new SanctionedEntity { Name = "B", Domicile = "Y", Accepted = false } + }; + var mockRepo = new Mock(); + mockRepo.Setup(r => r.GetSanctionedEntitiesAsync()).ReturnsAsync(entities); + var svc = new SanctionedEntityService(mockRepo.Object); + + var dtos = (await svc.ListAllAsync()).ToList(); + + dtos.Should().HaveCount(2); + dtos[0].Id.Should().Be(entities[0].Id); + dtos[0].Name.Should().Be("A"); + } + + [Fact] + public async Task AddAsync_Throws_When_Name_Or_Domicile_Empty() + { + var mockRepo = new Mock(); + var svc = new SanctionedEntityService(mockRepo.Object); + + Func act = () => svc.AddAsync(new EntityDto(Guid.Empty, "", "", true)); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task AddAsync_Throws_When_Duplicate() + { + var dto = new EntityDto(Guid.Empty, "Name", "Dom", true); + var mockRepo = new Mock(); + mockRepo.Setup(r => r.ExistsAsync(dto.Name, dto.Domicile)).ReturnsAsync(true); + var svc = new SanctionedEntityService(mockRepo.Object); + + Func act = () => svc.AddAsync(dto); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task AddAsync_Creates_Entity_When_Valid() + { + var dto = new EntityDto(Guid.Empty, "Name", "Dom", true); + var mockRepo = new Mock(); + mockRepo.Setup(r => r.ExistsAsync(dto.Name, dto.Domicile)).ReturnsAsync(false); + mockRepo.Setup(r => r.CreateSanctionedEntityAsync(It.IsAny())); + var svc = new SanctionedEntityService(mockRepo.Object); + + await svc.AddAsync(dto); + + mockRepo.Verify(r => r.CreateSanctionedEntityAsync( + It.Is(e => e.Name == dto.Name && e.Domicile == dto.Domicile)), Times.Once); + } + } +} diff --git a/AJGRE.Application.Tests/Usings.cs b/AJGRE.Application.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/AJGRE.Application.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/AJGRE.Application/AJGRE.Application.csproj b/AJGRE.Application/AJGRE.Application.csproj new file mode 100644 index 0000000..96752e3 --- /dev/null +++ b/AJGRE.Application/AJGRE.Application.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/AJGRE.Application/DTOs/EntityDto.cs b/AJGRE.Application/DTOs/EntityDto.cs new file mode 100644 index 0000000..b215028 --- /dev/null +++ b/AJGRE.Application/DTOs/EntityDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AJGRE.Application.DTOs +{ + public record EntityDto( + Guid Id, + string Name, + string Domicile, + bool Accepted +); +} diff --git a/AJGRE.Application/Interfaces/ISanctionedEntityService.cs b/AJGRE.Application/Interfaces/ISanctionedEntityService.cs new file mode 100644 index 0000000..f21e1ed --- /dev/null +++ b/AJGRE.Application/Interfaces/ISanctionedEntityService.cs @@ -0,0 +1,16 @@ +using AJGRE.Application.DTOs; +using ajgre_technical_interview.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AJGRE.Application.Interfaces +{ + public interface ISanctionedEntityService + { + Task> ListAllAsync(); + Task AddAsync(EntityDto dto); + } +} diff --git a/AJGRE.Application/Services/SanctionedEntityService.cs b/AJGRE.Application/Services/SanctionedEntityService.cs new file mode 100644 index 0000000..1e2f9b3 --- /dev/null +++ b/AJGRE.Application/Services/SanctionedEntityService.cs @@ -0,0 +1,39 @@ +using AJGRE.Application.DTOs; +using AJGRE.Application.Interfaces; +using ajgre_technical_interview.Models; +using AJRE.Domain.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AJGRE.Application.Services +{ + public class SanctionedEntityService : ISanctionedEntityService + { + private readonly ISanctionedEntityRepository _repo; + public SanctionedEntityService(ISanctionedEntityRepository repo) => _repo = repo; + + public async Task> ListAllAsync() + => (await _repo.GetSanctionedEntitiesAsync()) + .Select(e => new EntityDto(e.Id, e.Name, e.Domicile, e.Accepted)); + + public async Task AddAsync(EntityDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.Domicile)) + throw new ArgumentException("Name and Domicile are required."); + + if (await _repo.ExistsAsync(dto.Name, dto.Domicile)) + throw new InvalidOperationException("Duplicate entity."); + + var entity = new SanctionedEntity + { + Name = dto.Name, + Domicile = dto.Domicile, + Accepted = dto.Accepted + }; + return await _repo.CreateSanctionedEntityAsync(entity); + } + } +} diff --git a/AJGRE.Infrastructure/AJGRE.Infrastructure.csproj b/AJGRE.Infrastructure/AJGRE.Infrastructure.csproj new file mode 100644 index 0000000..cad8844 --- /dev/null +++ b/AJGRE.Infrastructure/AJGRE.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/AJGRE.Infrastructure/Configuration/DependencyInjection.cs b/AJGRE.Infrastructure/Configuration/DependencyInjection.cs new file mode 100644 index 0000000..ea50b7a --- /dev/null +++ b/AJGRE.Infrastructure/Configuration/DependencyInjection.cs @@ -0,0 +1,22 @@ +using AJGRE.Infrastructure.Repository; +using AJRE.Domain.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AJGRE.Infrastructure.Configuration +{ + public static class DependencyInjection + { + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + services.AddSingleton(); + + + return services; + } + } +} diff --git a/AJGRE.Infrastructure/Repository/SanctionedEntityRepository.cs b/AJGRE.Infrastructure/Repository/SanctionedEntityRepository.cs new file mode 100644 index 0000000..f835c40 --- /dev/null +++ b/AJGRE.Infrastructure/Repository/SanctionedEntityRepository.cs @@ -0,0 +1,51 @@ +using ajgre_technical_interview.Models; +using AJRE.Domain.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AJGRE.Infrastructure.Repository +{ + public class SanctionedEntityRepository : ISanctionedEntityRepository + { + private static readonly IList SanctionedEntities = new List + { + new SanctionedEntity { Name = "Forbidden Company", Domicile = "Mars", Accepted = false }, + new SanctionedEntity { Name = "Allowed Company", Domicile = "Venus", Accepted = true }, + new SanctionedEntity { Name = "Good Ltd", Domicile = "Saturn", Accepted = true }, + new SanctionedEntity { Name = "Evil Plc", Domicile = "Venus", Accepted = false } + }; + + public async Task> GetSanctionedEntitiesAsync() + { + var entities = SanctionedEntities + .OrderBy(e => e.Name) + .ThenBy(e => e.Domicile) + .ToList(); + + return await Task.FromResult(entities); + } + + public async Task GetSanctionedEntityByIdAsync(Guid id) + { + return await Task.FromResult( + SanctionedEntities.First(e => e.Id.Equals(id))); + } + + public async Task CreateSanctionedEntityAsync(SanctionedEntity sanctionedEntity) + { + SanctionedEntities.Add(sanctionedEntity); + return await Task.FromResult(sanctionedEntity); + } + + public async Task ExistsAsync(string name, string domicile) + { + var exists = SanctionedEntities.Any(e => + e.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && + e.Domicile.Equals(domicile, StringComparison.OrdinalIgnoreCase)); + return await Task.FromResult(exists); + } + } +} diff --git a/AJGRE.WebApi.Tests/AJGRE.WebApi.Tests.csproj b/AJGRE.WebApi.Tests/AJGRE.WebApi.Tests.csproj new file mode 100644 index 0000000..9edb29d --- /dev/null +++ b/AJGRE.WebApi.Tests/AJGRE.WebApi.Tests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/AJGRE.WebApi.Tests/Controllers/SanctionedEntitiesControllerTests.cs b/AJGRE.WebApi.Tests/Controllers/SanctionedEntitiesControllerTests.cs new file mode 100644 index 0000000..dcb211c --- /dev/null +++ b/AJGRE.WebApi.Tests/Controllers/SanctionedEntitiesControllerTests.cs @@ -0,0 +1,30 @@ +using AJGRE.Application.DTOs; +using AJGRE.Application.Interfaces; +using AJGRE.Application.Services; +using ajgre_technical_interview.Controllers; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace AJGRE.WebApi.Tests.Controllers +{ + public class SanctionedEntitiesControllerTests + { + [Fact] + public async Task ListAll_Returns_Ok() + { + var dtos = new List { new EntityDto(System.Guid.NewGuid(), "X", "Y", true) }; + var mockSvc = new Mock(MockBehavior.Strict); + mockSvc.Setup(s => s.ListAllAsync()).ReturnsAsync(dtos); + var ctrl = new SanctionedEntitiesController(mockSvc.Object); + + var result = await ctrl.ListAll(); + + result.Result.Should().BeOfType(); + (result.Result as OkObjectResult).Value.Should().BeEquivalentTo(dtos); + } + } +} diff --git a/AJGRE.WebApi.Tests/Usings.cs b/AJGRE.WebApi.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/AJGRE.WebApi.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/AJRE.Domain/AJGRE.Domain.csproj b/AJRE.Domain/AJGRE.Domain.csproj new file mode 100644 index 0000000..27ac386 --- /dev/null +++ b/AJRE.Domain/AJGRE.Domain.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/AJRE.Domain/Entities/SanctionedEntity.cs b/AJRE.Domain/Entities/SanctionedEntity.cs new file mode 100644 index 0000000..919945f --- /dev/null +++ b/AJRE.Domain/Entities/SanctionedEntity.cs @@ -0,0 +1,12 @@ +namespace ajgre_technical_interview.Models +{ + public class SanctionedEntity + { + public Guid Id { get; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; + public string Domicile { get; set; } = string.Empty; + public bool Accepted { get; set; } + + + } +} diff --git a/AJRE.Domain/Interfaces/ISanctionedEntityRepository.cs b/AJRE.Domain/Interfaces/ISanctionedEntityRepository.cs new file mode 100644 index 0000000..b30ae54 --- /dev/null +++ b/AJRE.Domain/Interfaces/ISanctionedEntityRepository.cs @@ -0,0 +1,17 @@ +using ajgre_technical_interview.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AJRE.Domain.Interfaces +{ + public interface ISanctionedEntityRepository + { + Task> GetSanctionedEntitiesAsync(); + Task GetSanctionedEntityByIdAsync(Guid id); + Task CreateSanctionedEntityAsync(SanctionedEntity entity); + Task ExistsAsync(string name, string domicile); + } +} diff --git a/ajgre-technical-interview.csproj b/ajgre-technical-interview.csproj index 242693f..7982860 100644 --- a/ajgre-technical-interview.csproj +++ b/ajgre-technical-interview.csproj @@ -17,8 +17,28 @@ + + + + + + + + + + + + + + + + + + + + @@ -33,8 +53,8 @@ - - + + diff --git a/ajgre-technical-interview.sln b/ajgre-technical-interview.sln index 6706635..d26de07 100644 --- a/ajgre-technical-interview.sln +++ b/ajgre-technical-interview.sln @@ -5,17 +5,17 @@ VisualStudioVersion = 17.5.33627.172 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ajgre-technical-interview", "ajgre-technical-interview.csproj", "{4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Domain", "..\AJRE.Domain\AJGRE.Domain.csproj", "{5B3E0263-51F6-4271-8222-AA6187CB0AD2}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Application", "..\AJGRE.Application\AJGRE.Application.csproj", "{A80B80A4-5858-44AB-A6E8-92731A18FAFF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Application.Tests", "AJGRE.Application.Tests\AJGRE.Application.Tests.csproj", "{4BE9028F-2EEA-9E18-144F-1EA36C644BC6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Infrastructure", "..\AJGRE.Infrastructure\AJGRE.Infrastructure.csproj", "{00D2D57C-2495-4766-A38D-D8AB3571F10E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.WebApi.Tests", "AJGRE.WebApi.Tests\AJGRE.WebApi.Tests.csproj", "{C7A2EEEF-C144-96D7-D890-10D19EE51EB7}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Application", "AJGRE.Application\AJGRE.Application.csproj", "{245DD1EC-B910-D04C-3C4B-2CB80CECD309}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Application.Tests", "..\AJGRE.Application.Tests\AJGRE.Application.Tests.csproj", "{4235CF2A-C986-4C38-9397-5762F576172B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Infrastructure", "AJGRE.Infrastructure\AJGRE.Infrastructure.csproj", "{74436365-7120-626E-9AA5-50AB86EE5FD4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.WebApi.Tests", "..\AJGRE.WebApi.Tests\AJGRE.WebApi.Tests.csproj", "{51648979-BF80-41D2-9752-1BD97B1DE56B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AJGRE.Domain", "AJRE.Domain\AJGRE.Domain.csproj", "{E56BA8A6-614B-0836-BA4A-CD1980038427}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,33 +27,33 @@ Global {4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C0BCE9E-A4A4-4A97-BB77-3EE7699F037A}.Release|Any CPU.Build.0 = Release|Any CPU - {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B3E0263-51F6-4271-8222-AA6187CB0AD2}.Release|Any CPU.Build.0 = Release|Any CPU - {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A80B80A4-5858-44AB-A6E8-92731A18FAFF}.Release|Any CPU.Build.0 = Release|Any CPU - {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00D2D57C-2495-4766-A38D-D8AB3571F10E}.Release|Any CPU.Build.0 = Release|Any CPU - {4235CF2A-C986-4C38-9397-5762F576172B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4235CF2A-C986-4C38-9397-5762F576172B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4235CF2A-C986-4C38-9397-5762F576172B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4235CF2A-C986-4C38-9397-5762F576172B}.Release|Any CPU.Build.0 = Release|Any CPU - {51648979-BF80-41D2-9752-1BD97B1DE56B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51648979-BF80-41D2-9752-1BD97B1DE56B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51648979-BF80-41D2-9752-1BD97B1DE56B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51648979-BF80-41D2-9752-1BD97B1DE56B}.Release|Any CPU.Build.0 = Release|Any CPU + {4BE9028F-2EEA-9E18-144F-1EA36C644BC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BE9028F-2EEA-9E18-144F-1EA36C644BC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4BE9028F-2EEA-9E18-144F-1EA36C644BC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BE9028F-2EEA-9E18-144F-1EA36C644BC6}.Release|Any CPU.Build.0 = Release|Any CPU + {C7A2EEEF-C144-96D7-D890-10D19EE51EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7A2EEEF-C144-96D7-D890-10D19EE51EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7A2EEEF-C144-96D7-D890-10D19EE51EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7A2EEEF-C144-96D7-D890-10D19EE51EB7}.Release|Any CPU.Build.0 = Release|Any CPU + {245DD1EC-B910-D04C-3C4B-2CB80CECD309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {245DD1EC-B910-D04C-3C4B-2CB80CECD309}.Debug|Any CPU.Build.0 = Debug|Any CPU + {245DD1EC-B910-D04C-3C4B-2CB80CECD309}.Release|Any CPU.ActiveCfg = Release|Any CPU + {245DD1EC-B910-D04C-3C4B-2CB80CECD309}.Release|Any CPU.Build.0 = Release|Any CPU + {74436365-7120-626E-9AA5-50AB86EE5FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74436365-7120-626E-9AA5-50AB86EE5FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74436365-7120-626E-9AA5-50AB86EE5FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74436365-7120-626E-9AA5-50AB86EE5FD4}.Release|Any CPU.Build.0 = Release|Any CPU + {E56BA8A6-614B-0836-BA4A-CD1980038427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E56BA8A6-614B-0836-BA4A-CD1980038427}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E56BA8A6-614B-0836-BA4A-CD1980038427}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E56BA8A6-614B-0836-BA4A-CD1980038427}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {4235CF2A-C986-4C38-9397-5762F576172B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {51648979-BF80-41D2-9752-1BD97B1DE56B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4BE9028F-2EEA-9E18-144F-1EA36C644BC6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C7A2EEEF-C144-96D7-D890-10D19EE51EB7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CF39E593-2D0F-4B41-B320-443070DDD858}