Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
using FluentAssertions;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Moq;
using Moq.Protected;
using Paymentsense.Coding.Challenge.Api.Controllers;
using Paymentsense.Coding.Challenge.Api.Interfaces;
using Paymentsense.Coding.Challenge.Api.Services;
using Xunit;

namespace Paymentsense.Coding.Challenge.Api.Tests.Controllers
Expand All @@ -11,12 +21,62 @@ public class PaymentsenseCodingChallengeControllerTests
[Fact]
public void Get_OnInvoke_ReturnsExpectedMessage()
{
var controller = new PaymentsenseCodingChallengeController();
var countryRepoServiceMock = new Mock<ICountryDataProvider>();
var controller = new PaymentsenseCodingChallengeController(countryRepoServiceMock.Object);

var result = controller.Get().Result as OkObjectResult;

result.StatusCode.Should().Be(StatusCodes.Status200OK);
result.Value.Should().Be("Paymentsense Coding Challenge!");
}

[Fact]
public void CountryList_MissingDataSource_ReturnsErrorMessage()
{
// Mock configuration to return wrong config key
var configurationMock = new Mock<IConfiguration>();
var configurationSection = new Mock<IConfigurationSection>();
configurationSection.Setup(a => a.Value).Returns("testvalue");
configurationMock.Setup(a => a.GetSection("TestValueKey")).Returns(configurationSection.Object);

var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var countryRepoService = new CountryDataProvider(httpClientFactoryMock.Object, configurationMock.Object);
var controller = new PaymentsenseCodingChallengeController(countryRepoService);

Assert.ThrowsAsync<ApplicationException>(() => controller.CountryList());
}

[Fact]
public async Task CountryList_WrongDataSource_ReturnsNullMessage()
{
// Mock configuration to return wrong data source url
var configurationMock = new Mock<IConfiguration>();
var configurationSection = new Mock<IConfigurationSection>();
configurationSection.Setup(a => a.Value).Returns("test123");
configurationMock.Setup(a => a.GetSection("DataSource")).Returns(configurationSection.Object);

var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var mockHttpMessageHandlerMock = new Mock<HttpMessageHandler>();

mockHttpMessageHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
});

var client = new HttpClient(mockHttpMessageHandlerMock.Object)
{
BaseAddress = new Uri("https://restcountries.eu/rest/v2/all"),
};
httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);

var countryRepoService = new CountryDataProvider(httpClientFactoryMock.Object, configurationMock.Object);
var controller = new PaymentsenseCodingChallengeController(countryRepoService);

var result = await controller.CountryList() as ObjectResult;
Assert.Null(result.Value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="Moq" Version="4.15.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Paymentsense.Coding.Challenge.Api.Interfaces;

namespace Paymentsense.Coding.Challenge.Api.Controllers
{
[ApiController]
[Route("[controller]")]
public class PaymentsenseCodingChallengeController : ControllerBase
{
public ICountryDataProvider _countryRepositoryService { get; }

public PaymentsenseCodingChallengeController(ICountryDataProvider countryRepositoryService)
{
_countryRepositoryService = countryRepositoryService;
}

[HttpGet]
public ActionResult<string> Get()
{
return Ok("Paymentsense Coding Challenge!");
}

[ResponseCache(Duration = int.MaxValue)]
[Route("countries/list")]
[HttpGet]
public async Task<ActionResult> CountryList()
{
var list = await _countryRepositoryService.GetCountryList();
return Ok(list);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Paymentsense.Coding.Challenge.Api.Models;

namespace Paymentsense.Coding.Challenge.Api.Interfaces
{
public interface ICountryDataProvider
{
Task<Country[]> GetCountryList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Collections.Generic;

namespace Paymentsense.Coding.Challenge.Api.Models
{
public class Country
{
public string Name { get; set; }
public string Flag { get; set; }
public int Population { get; set; }
public List<string> Timezones { get; set; }
public List<Currency> Currencies { get; set; }
public List<Language> Languages { get; set; }
public string Capital { get; set; }
}

public class Currency
{
public string Code { get; set; }
public string Name { get; set; }
public string Symbol { get; set; }
}

public class Language
{
public string Iso639_1 { get; set; }
public string Iso639_2 { get; set; }
public string Name { get; set; }
public string NativeName { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
</PropertyGroup>

<ItemGroup>
<Folder Include="Models\" />
<Folder Include="Services\" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Paymentsense.Coding.Challenge.Api.Interfaces;
using Paymentsense.Coding.Challenge.Api.Models;

namespace Paymentsense.Coding.Challenge.Api.Services
{
public class CountryDataProvider : ICountryDataProvider
{
private readonly IHttpClientFactory _clientFactory;
private readonly IConfiguration _configuration;

public CountryDataProvider(IHttpClientFactory clientFactory, IConfiguration configuration)
{
_clientFactory = clientFactory;
_configuration = configuration;
}

public async Task<Country[]> GetCountryList()
{
// Get country data source url from config
var sourceUrl = _configuration.GetValue<string>("DataSource");
if (string.IsNullOrEmpty(sourceUrl))
{
throw new ApplicationException("Country data source url missing in configuration file");
}

// Make call to fetch country list
var client = _clientFactory.CreateClient();
var response = await client.GetAsync(sourceUrl);
if (response != null && response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Country[]>(content);
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Paymentsense.Coding.Challenge.Api.Interfaces;
using Paymentsense.Coding.Challenge.Api.Services;

namespace Paymentsense.Coding.Challenge.Api
{
Expand All @@ -29,6 +31,13 @@ public void ConfigureServices(IServiceCollection services)
.AllowAnyHeader();
});
});

// Register country data provider
services.AddScoped<ICountryDataProvider, CountryDataProvider>();
services.AddHttpClient();

// Enable response caching
services.AddResponseCaching();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand All @@ -52,6 +61,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
});

// Activate response caching middleware
app.UseResponseCaching();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"DataSource": "https://restcountries.eu/rest/v2/all"
}
24 changes: 20 additions & 4 deletions paymentsense-coding-challenge-website/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<img width="50%" alt="Paymentsense Logo" src="../assets/paymentsense-logo.svg">
<div style="text-align: center;">
<img
width="50%"
alt="Paymentsense Logo"
src="../assets/paymentsense-logo.svg"
/>
<h1>
{{ title }}
</h1>
<h2>... Paymentsense Coding Challenge API is <fa-icon [icon]=paymentsenseCodingChallengeApiActiveIcon
[styles]="{ 'stroke': paymentsenseCodingChallengeApiActiveIconColour, 'color': paymentsenseCodingChallengeApiActiveIconColour }"></fa-icon> ...</h2>
<h2>
... Paymentsense Coding Challenge API is
<fa-icon
[icon]="paymentsenseCodingChallengeApiActiveIcon"
[styles]="{
stroke: paymentsenseCodingChallengeApiActiveIconColour,
color: paymentsenseCodingChallengeApiActiveIconColour
}"
></fa-icon>
...
</h2>

<hr />
<country-list></country-list>
</div>

<router-outlet></router-outlet>
44 changes: 27 additions & 17 deletions paymentsense-coding-challenge-website/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { PaymentsenseCodingChallengeApiService } from './services';
import { MockPaymentsenseCodingChallengeApiService } from './testing/mock-paymentsense-coding-challenge-api.service';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { TestBed, async } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { AppComponent } from "./app.component";
import { PaymentsenseCodingChallengeApiService } from "./services";
import { MockPaymentsenseCodingChallengeApiService } from "./testing/mock-paymentsense-coding-challenge-api.service";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { CountryListComponent } from "./country-list/country-list.component";
import { MatTableModule } from "@angular/material/table";
import { MatPaginatorModule } from "@angular/material/paginator";
import { HttpClientModule } from "@angular/common/http";

describe('AppComponent', () => {
describe("AppComponent", () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FontAwesomeModule
],
declarations: [
AppComponent
FontAwesomeModule,
MatTableModule,
MatPaginatorModule,
HttpClientModule,
],
declarations: [AppComponent, CountryListComponent],
providers: [
{ provide: PaymentsenseCodingChallengeApiService, useClass: MockPaymentsenseCodingChallengeApiService }
]
{
provide: PaymentsenseCodingChallengeApiService,
useClass: MockPaymentsenseCodingChallengeApiService,
},
],
}).compileComponents();
}));

it('should create the app', () => {
it("should create the app", () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
Expand All @@ -30,13 +38,15 @@ describe('AppComponent', () => {
it(`should have as title 'Paymentsense Coding Challenge'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('Paymentsense Coding Challenge!');
expect(app.title).toEqual("Paymentsense Coding Challenge!");
});

it('should render title in a h1 tag', () => {
it("should render title in a h1 tag", () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Paymentsense Coding Challenge!');
expect(compiled.querySelector("h1").textContent).toContain(
"Paymentsense Coding Challenge!"
);
});
});
Loading