diff --git a/BackendCandidateChallenge/QuizClient.Tests/QuizClient.Tests.csproj b/BackendCandidateChallenge/QuizClient.Tests/QuizClient.Tests.csproj index f4760d5..dfa5e72 100644 --- a/BackendCandidateChallenge/QuizClient.Tests/QuizClient.Tests.csproj +++ b/BackendCandidateChallenge/QuizClient.Tests/QuizClient.Tests.csproj @@ -1,14 +1,17 @@  - net7.0 + net8 - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/BackendCandidateChallenge/QuizClient.Tests/QuizClientTests.cs b/BackendCandidateChallenge/QuizClient.Tests/QuizClientTests.cs index d23d58a..1a528cc 100644 --- a/BackendCandidateChallenge/QuizClient.Tests/QuizClientTests.cs +++ b/BackendCandidateChallenge/QuizClient.Tests/QuizClientTests.cs @@ -1,291 +1,280 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using PactNet.Mocks.MockHttpService; -using PactNet.Mocks.MockHttpService.Models; +using Moq; +using PactNet; +using PactNet.Output.Xunit; using Xunit; +using Xunit.Abstractions; namespace QuizClient.Tests; -public class QuizClientTests : IClassFixture +public class QuizClientTests { - private readonly IMockProviderService _mockProviderService; - private readonly Uri _mockProviderServiceBaseUri; - private static readonly HttpClient Client = new HttpClient(); + private readonly IPactBuilderV4 _pact; + private readonly Mock _mockFactory; - public QuizClientTests(QuizServiceApiPact data) + public QuizClientTests(ITestOutputHelper testOutput) { - _mockProviderService = data.MockProviderService; - _mockProviderService.ClearInteractions(); - _mockProviderServiceBaseUri = data.MockProviderServiceBaseUri; + var config = new PactConfig + { + PactDir = @"..\pacts", + Outputters = + [ + new XunitOutput(testOutput) + ], + DefaultJsonSettings = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + } + }; + + _pact = Pact.V4("QuizClient", "QuizService", config).WithHttpInteractions(); + + _mockFactory = new Mock(); } [Fact] public async Task GetQuizzes_WhenSomeQuizzesExists_ReturnsTheQuizzes() { - _mockProviderService - .Given("There are some quizzes") + _pact .UponReceiving("A GET request to retrieve the quizzes") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Get, - Path = "/api/quizzes", - Headers = new Dictionary - { - { "Accept", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 200, - Headers = new Dictionary - { - { "Content-Type", "application/json; charset=utf-8" } - }, - Body = new[] + .Given("There are some quizzes") + .WithRequest(HttpMethod.Get, "/api/quizzes") + .WithHeader("Accept", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json; charset=utf-8") + .WithJsonBody(new[] { new { id = 123, title = "This is quiz 123" }, - new { + new + { id = 124, title = "This is quiz 124" } - } - }); + }); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.GetQuizzesAsync(CancellationToken.None); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.OK, result.StatusCode); - Assert.NotEmpty(result.Value); - Assert.Equal(2, result.Value.Count()); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri, _mockFactory.Object.CreateClient("Quiz")); + var result = await client.GetQuizzesAsync(CancellationToken.None); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.NotEmpty(result.Value); + Assert.Equal(2, result.Value.Count()); + }); } [Fact] public async Task GetQuiz_WhenAQuizWExists_ReturnsTheQuiz() { - _mockProviderService - .Given("There are some quizzes") + _pact .UponReceiving("A GET request to retrieve the quiz") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Get, - Path = "/api/quizzes/123", - Headers = new Dictionary - { - { "Accept", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 200, - Headers = new Dictionary - { - { "Content-Type", "application/json; charset=utf-8" } - }, - Body = new + .Given("There are some quizzes") + .WithRequest(HttpMethod.Get, "/api/quizzes/123") + .WithHeader("Accept", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json; charset=utf-8") + .WithJsonBody(new { id = 123, title = "This is quiz 123" - } - }); + }); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.GetQuizAsync(123, CancellationToken.None); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.OK, result.StatusCode); - Assert.NotEqual(Quiz.NotFound, result.Value); - Assert.Equal("This is quiz 123", result.Value.Title); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri,_mockFactory.Object.CreateClient("Quiz")); + var result = await client.GetQuizAsync(123, CancellationToken.None); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.NotEqual(Quiz.NotFound, result.Value); + Assert.Equal("This is quiz 123", result.Value.Title); + }); } [Fact] public async Task PostQuiz_Returns201CreatedAndLocationHeader() { - _mockProviderService - .Given("There are some quizzes") + _pact .UponReceiving("A POST quiz request") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Post, - Path = "/api/quizzes", - Headers = new Dictionary - { - { "Content-Type", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 201, - Headers = new Dictionary - { - { "Content-Type", "application/json" }, - { "Location", PactNet.Matchers.Match.Regex("/api/quizzes/1", "quizzes\\/[0-9]*") } - } - }); + .Given("There are some quizzes") + .WithRequest(HttpMethod.Post, "/api/quizzes") + .WithHeader("Content-Type", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.Created) + .WithHeader("Content-Type", "application/json") + .WithHeader("Location", PactNet.Matchers.Match.Regex("/api/quizzes/1", "quizzes\\/[0-9]*")); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.PostQuizAsync(new Quiz { Title = "This is quiz 999" }, CancellationToken.None); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.Created, result.StatusCode); - Assert.NotNull(result.Value); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri,_mockFactory.Object.CreateClient("Quiz")); + var result = await client.PostQuizAsync(new Quiz { Title = "This is quiz 999" }, CancellationToken.None); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + Assert.NotNull(result.Value); + }); } [Fact] public async Task PostQuestion_Returns201CreatedAndLocationHeader() { - _mockProviderService - .Given("There are some quizzes") + _pact .UponReceiving("A POST request to quiz 123 questions collection") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Post, - Path = "/api/quizzes/123/questions", - Headers = new Dictionary - { - { "Content-Type", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 201, - Headers = new Dictionary - { - { "Content-Type", "application/json" }, - { "Location", PactNet.Matchers.Match.Regex("/api/quizzes/123/questions/1", "quizzes\\/123\\/questions\\/[0-9]*") } - } - }); + .Given("There are some quizzes") + .WithRequest(HttpMethod.Post, "/api/quizzes/123/questions") + .WithHeader("Content-Type", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.Created) + .WithHeader("Content-Type", "application/json") + .WithHeader("Location", PactNet.Matchers.Match.Regex("/api/quizzes/123/questions/1", "quizzes\\/123\\/questions\\/[0-9]*")); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.PostQuestionAsync(123, new QuizQuestion { Text = "This is a question" }, CancellationToken.None); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.Created, result.StatusCode); - Assert.NotNull(result.Value); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri,_mockFactory.Object.CreateClient("Quiz")); + var result = await client.PostQuestionAsync(123, new QuizQuestion { Text = "This is a question" }, CancellationToken.None); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + Assert.NotNull(result.Value); + }); } [Fact] public async Task PutQuestion_WhenAQuestionWExists_UpdatesTheQuestion() { - _mockProviderService - .Given("There are some quizzes") + _pact .UponReceiving("A PUT request to update a quiz question with id = 1") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Put, - Path = "/api/quizzes/123/questions/1", - Headers = new Dictionary - { - { "Content-Type", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 204, - Headers = new Dictionary - { - { "Content-Type", "application/json; charset=utf-8" } - } - }); + .Given("There are some quizzes") + .WithRequest(HttpMethod.Put, "/api/quizzes/123/questions/1") + .WithHeader("Content-Type", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.NoContent) + .WithHeader("Content-Type", "application/json; charset=utf-8"); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.PutQuestionAsync(123, 1, new QuizQuestion { Text = "Updated text" }, CancellationToken.None); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.NoContent, result.StatusCode); - Assert.NotEqual(Quiz.NotFound, result.Value); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri,_mockFactory.Object.CreateClient("Quiz")); + var result = await client.PutQuestionAsync(123, 1, new QuizQuestion { Text = "Updated text" }, + CancellationToken.None); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.NoContent, result.StatusCode); + Assert.NotEqual(Quiz.NotFound, result.Value); + }); } [Fact] public async Task PostAnswers_Returns201CreatedAndLocationHeader() { - _mockProviderService - .Given("There are some quizzes") + _pact .UponReceiving("A POST request") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Post, - Path = "/api/quizzes", - Headers = new Dictionary - { - { "Content-Type", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 201, - Headers = new Dictionary - { - { "Content-Type", "application/json" }, - { "Location", PactNet.Matchers.Match.Regex("/api/quizzes/1", "quizzes\\/[0-9]*") } - }, - Body = new + .Given("There are some quizzes") + .WithRequest(HttpMethod.Post, "/api/quizzes") + .WithHeader("Content-Type", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.Created) + .WithHeader("Content-Type", "application/json") + .WithHeader("Location", PactNet.Matchers.Match.Regex("/api/quizzes/1", "quizzes\\/[0-9]*")) + .WithJsonBody(new { title = "This is quiz 999" - } - }); + }); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.PostQuizAsync(new Quiz { Title = "This is quiz 999" }, CancellationToken.None); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.Created, result.StatusCode); - Assert.NotNull(result.Value); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri,_mockFactory.Object.CreateClient("Quiz")); + var result = await client.PostQuizAsync(new Quiz { Title = "This is quiz 999" }, CancellationToken.None); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + Assert.NotNull(result.Value); + }); } [Fact] public async Task GivenThatAQuizExistsPostingAnAnswerCreatesAQuizResponse() { - _mockProviderService - .Given("There is a quiz with id '123'") + _pact .UponReceiving("A POST request creates a quiz response") - .With(new ProviderServiceRequest - { - Method = HttpVerb.Post, - Path = "/api/quizzes/123/responses", - Headers = new Dictionary - { - { "Content-Type", "application/json" } - } - }) - .WillRespondWith(new ProviderServiceResponse - { - Status = 201, - Headers = new Dictionary - { - { "Content-Type", "application/json" }, - { "Location", PactNet.Matchers.Match.Regex("/api/quizzes/123/responses/1", "responses\\/[0-9]*") } - } - }); + .Given("There is a quiz with id '123'") + .WithRequest(HttpMethod.Post, "/api/quizzes/123/responses") + .WithHeader("Content-Type", "application/json") + .WillRespond() + .WithStatus(HttpStatusCode.Created) + .WithHeader("Content-Type", "application/json") + .WithHeader("Location", PactNet.Matchers.Match.Regex("/api/quizzes/123/responses/1", "responses\\/[0-9]*")); - var consumer = new QuizClient(_mockProviderServiceBaseUri, Client); - - var result = await consumer.PostQuizResponseAsync(new QuestionResponse(),123); - Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); - Assert.Equal(HttpStatusCode.Created, result.StatusCode); - Assert.NotNull(result.Value); + await _pact.VerifyAsync(async ctx => + { + _mockFactory + .Setup(f => f.CreateClient("Quiz")) + .Returns(() => new HttpClient + { + BaseAddress = ctx.MockServerUri, + DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") } } + }); - _mockProviderService.VerifyInteractions(); + var client = new QuizClient(ctx.MockServerUri,_mockFactory.Object.CreateClient("Quiz")); + var result = await client.PostQuizResponseAsync(new QuestionResponse(), 123); + Assert.True(string.IsNullOrEmpty(result.ErrorMessage), result.ErrorMessage); + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + Assert.NotNull(result.Value); + }); } } \ No newline at end of file diff --git a/BackendCandidateChallenge/QuizClient.Tests/QuizServiceApiPact.cs b/BackendCandidateChallenge/QuizClient.Tests/QuizServiceApiPact.cs deleted file mode 100644 index a9d181b..0000000 --- a/BackendCandidateChallenge/QuizClient.Tests/QuizServiceApiPact.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using PactNet; -using PactNet.Mocks.MockHttpService; - -namespace QuizClient.Tests; - -public class QuizServiceApiPact : IDisposable -{ - public IPactBuilder PactBuilder { get; } - public IMockProviderService MockProviderService { get; } - - public int MockServerPort => 9222; - public Uri MockProviderServiceBaseUri => new UriBuilder(Uri.UriSchemeHttp, "localhost", MockServerPort).Uri; - - public QuizServiceApiPact() - { - PactBuilder = new PactBuilder(new PactConfig { SpecificationVersion = "2.0.0", PactDir = @"..\pacts" }); - - PactBuilder - .ServiceConsumer("QuizClient") - .HasPactWith("QuizService"); - - MockProviderService = PactBuilder.MockService(MockServerPort); - } - - public void Dispose() - { - PactBuilder.Build(); - } -} \ No newline at end of file diff --git a/BackendCandidateChallenge/QuizClient/QuizClient.csproj b/BackendCandidateChallenge/QuizClient/QuizClient.csproj index 76a03a9..de93036 100644 --- a/BackendCandidateChallenge/QuizClient/QuizClient.csproj +++ b/BackendCandidateChallenge/QuizClient/QuizClient.csproj @@ -1,11 +1,11 @@  - net7.0 + net8 - + diff --git a/BackendCandidateChallenge/QuizService.Tests/QuizService.Tests.csproj b/BackendCandidateChallenge/QuizService.Tests/QuizService.Tests.csproj index 2aaa4dc..541bd2a 100644 --- a/BackendCandidateChallenge/QuizService.Tests/QuizService.Tests.csproj +++ b/BackendCandidateChallenge/QuizService.Tests/QuizService.Tests.csproj @@ -1,15 +1,15 @@  - net7.0 + net8 - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/BackendCandidateChallenge/QuizService/QuizService.csproj b/BackendCandidateChallenge/QuizService/QuizService.csproj index e0eed45..b59b4e5 100644 --- a/BackendCandidateChallenge/QuizService/QuizService.csproj +++ b/BackendCandidateChallenge/QuizService/QuizService.csproj @@ -1,13 +1,13 @@  - net7.0 + net8 - - - + + + diff --git a/Readme.md b/Readme.md index 9fe9f47..5f52e2a 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ This is a simple challenge to be used as part of an interview process for .NET b ## Rules 1. **Keep the initial commits** free from your own changes, so we can use git log and git show to see what you did. -2. You are free to use any tool you like when developing. Important note: as we are targeting .NET 7 it's not possible to build the solution in VS 2019. Potential alternatives include **VS 2022** and **VS Code**. +2. You are free to use any tool you like when developing. Important note: as we are targeting .NET 8 it's not possible to build the solution in VS 2019. Potential alternatives include **VS 2022** and **VS Code**. 3. You can use as much time as you like, but the intention is not to have you spend more than a couple of hours. 4. You are free to pull in any framework or libraries in order to solve the challenge, but be prepared to reason about your choices. 5. Use the patterns and practices you think are best on any part of the challenge, again be prepared to reason about your choices.