From 537d77e74aea04ed4deb1945d2c421adc10a4e06 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 01:17:46 +0200 Subject: [PATCH 01/12] TEST-0003 Change location of blogging context --- src/backend/WebApi/{ => Database}/BloggingContext.cs | 9 ++++++++- src/backend/WebApi/Entities/BlogDto.cs | 1 + src/backend/WebApi/Entities/PostDto.cs | 1 + .../20240618183910_InitialMigration.Designer.cs | 2 +- .../20240622201804_AddBlogMaxLength.Designer.cs | 2 +- .../WebApi/Migrations/BloggingContextModelSnapshot.cs | 2 +- src/backend/WebApi/Program.cs | 2 +- src/backend/WebApi/Repository/BloggingRepository.cs | 5 ++++- 8 files changed, 18 insertions(+), 6 deletions(-) rename src/backend/WebApi/{ => Database}/BloggingContext.cs (92%) diff --git a/src/backend/WebApi/BloggingContext.cs b/src/backend/WebApi/Database/BloggingContext.cs similarity index 92% rename from src/backend/WebApi/BloggingContext.cs rename to src/backend/WebApi/Database/BloggingContext.cs index e87ee9a2..44d167c6 100644 --- a/src/backend/WebApi/BloggingContext.cs +++ b/src/backend/WebApi/Database/BloggingContext.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace WebApi; +namespace WebApi.Database; /// /// Represents the database context for blogging, configuring entities and their relationships. @@ -31,11 +31,13 @@ public interface IBlog /// /// Gets or sets the unique identifier for the blog. /// + /// 1 public int BlogId { get; set; } /// /// Gets or sets the URL of the blog. /// + /// https://localhost/a-test-blog public string Url { get; set; } } @@ -76,21 +78,25 @@ public interface IPost /// /// Gets or sets the unique identifier for the post. /// + /// 1 public int PostId { get; set; } /// /// Gets or sets the title of the post. /// + /// Bob Loblaw's Law Blog public string Title { get; set; } /// /// Gets or sets the content of the post. /// + /// Example blog post content public string Content { get; set; } /// /// Gets or sets the unique identifier of the blog to which the post belongs. /// + /// 1 public int BlogId { get; set; } } @@ -102,6 +108,7 @@ public class Post : IPost /// Gets or sets the blog to which the post belongs. /// public Blog? Blog { get; set; } + /// public required int BlogId { get; set; } diff --git a/src/backend/WebApi/Entities/BlogDto.cs b/src/backend/WebApi/Entities/BlogDto.cs index b7fd951f..e4f62eff 100644 --- a/src/backend/WebApi/Entities/BlogDto.cs +++ b/src/backend/WebApi/Entities/BlogDto.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using WebApi.Database; namespace WebApi.Entities; diff --git a/src/backend/WebApi/Entities/PostDto.cs b/src/backend/WebApi/Entities/PostDto.cs index b3b68410..6377c09e 100644 --- a/src/backend/WebApi/Entities/PostDto.cs +++ b/src/backend/WebApi/Entities/PostDto.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using WebApi.Database; namespace WebApi.Entities; diff --git a/src/backend/WebApi/Migrations/20240618183910_InitialMigration.Designer.cs b/src/backend/WebApi/Migrations/20240618183910_InitialMigration.Designer.cs index e7d6a858..b990eda9 100644 --- a/src/backend/WebApi/Migrations/20240618183910_InitialMigration.Designer.cs +++ b/src/backend/WebApi/Migrations/20240618183910_InitialMigration.Designer.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using WebApi; +using WebApi.Database; #nullable disable diff --git a/src/backend/WebApi/Migrations/20240622201804_AddBlogMaxLength.Designer.cs b/src/backend/WebApi/Migrations/20240622201804_AddBlogMaxLength.Designer.cs index 48270d33..1e9acbf5 100644 --- a/src/backend/WebApi/Migrations/20240622201804_AddBlogMaxLength.Designer.cs +++ b/src/backend/WebApi/Migrations/20240622201804_AddBlogMaxLength.Designer.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using WebApi; +using WebApi.Database; #nullable disable diff --git a/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs b/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs index 65c27aef..6b6cb013 100644 --- a/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs +++ b/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using WebApi; +using WebApi.Database; #nullable disable diff --git a/src/backend/WebApi/Program.cs b/src/backend/WebApi/Program.cs index f594380f..0aab8126 100644 --- a/src/backend/WebApi/Program.cs +++ b/src/backend/WebApi/Program.cs @@ -10,7 +10,7 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Swashbuckle.AspNetCore.SwaggerUI; -using WebApi; +using WebApi.Database; using WebApi.Filters; using WebApi.Messaging; using WebApi.Middleware; diff --git a/src/backend/WebApi/Repository/BloggingRepository.cs b/src/backend/WebApi/Repository/BloggingRepository.cs index ce2daf5c..4a9633b2 100644 --- a/src/backend/WebApi/Repository/BloggingRepository.cs +++ b/src/backend/WebApi/Repository/BloggingRepository.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using WebApi.Database; using WebApi.Entities; namespace WebApi.Repository; @@ -104,6 +105,7 @@ public class BloggingRepository(BloggingContext bloggingContext) : IBloggingRepo // Update blogEntity.Url = blog.Url; + await bloggingContext.SaveChangesAsync(cancellationToken); return BlogDto.FromEntity(blogEntity); } @@ -114,6 +116,7 @@ public class BloggingRepository(BloggingContext bloggingContext) : IBloggingRepo Url = blog.Url }); + await bloggingContext.SaveChangesAsync(cancellationToken); return BlogDto.FromEntity(createdBlogEntity.Entity); } @@ -154,6 +157,7 @@ public async Task> ListPostsAsync(int blogId, CancellationT // Update existing post postEntity.Title = post.Title; postEntity.Content = post.Content; + await bloggingContext.SaveChangesAsync(cancellationToken); return PostDto.FromEntity(postEntity); } @@ -167,7 +171,6 @@ public async Task> ListPostsAsync(int blogId, CancellationT }); await bloggingContext.SaveChangesAsync(cancellationToken); - return PostDto.FromEntity(createdPostEntity.Entity); } } \ No newline at end of file From 246d4afc74ad60a3a58380618ca3ee76b8078155 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 01:25:33 +0200 Subject: [PATCH 02/12] TEST-0003 Add developer db configuration to launch --- src/backend/WebApi/Properties/launchSettings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/WebApi/Properties/launchSettings.json b/src/backend/WebApi/Properties/launchSettings.json index 4bb1b1af..d439133a 100644 --- a/src/backend/WebApi/Properties/launchSettings.json +++ b/src/backend/WebApi/Properties/launchSettings.json @@ -33,7 +33,8 @@ "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "DB_CONNECTION": "Server=127.0.0.1,1433;Initial Catalog=api;User=sa;Password=DevelopmentOnlyPassword1;TrustServerCertificate=True;" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7156;http://localhost:5208" From f326ec1e2f47fb775954ccf9747453c6e119599d Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 01:25:55 +0200 Subject: [PATCH 03/12] TEST-0003 Add blogging api example values --- .../WebApi/Controllers/BloggingController.cs | 35 ++++++++++----- src/backend/WebApi/swagger.json | 44 ++++++++++++------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/backend/WebApi/Controllers/BloggingController.cs b/src/backend/WebApi/Controllers/BloggingController.cs index 1ef4e8f6..2754a15e 100644 --- a/src/backend/WebApi/Controllers/BloggingController.cs +++ b/src/backend/WebApi/Controllers/BloggingController.cs @@ -12,9 +12,10 @@ namespace WebApi.Controllers; public class BloggingController(ILogger logger, IBloggingRepository bloggingRepository) : ControllerBase { /// - /// Get blogs + /// Gets a list of all blogs. /// - /// + /// Cancellation token to cancel the request. + /// A list of BlogDto objects wrapped in an ActionResult. [HttpGet("blog", Name = "GetBlogs")] public async Task>> GetBlogs(CancellationToken cancellationToken) { @@ -23,9 +24,11 @@ public async Task>> GetBlogs(CancellationToken } /// - /// Get specific blog + /// Gets a specific blog by ID. /// - /// + /// The ID of the blog. + /// Cancellation token to cancel the request. + /// A single BlogDto object wrapped in an ActionResult. Returns NotFound if the blog does not exist. [HttpGet("blog/{id:int}", Name = "GetBlog")] public async Task> GetBlog(int id, CancellationToken cancellationToken) { @@ -38,9 +41,11 @@ public async Task> GetBlog(int id, CancellationToken cance } /// - /// Create or update blog + /// Creates a new blog or updates an existing one. /// - /// + /// The blog data transfer object + /// Cancellation token + /// Returns the created or updated blog [HttpPost("blog", Name = "PostBlog")] public async Task> PostBlog(BlogDto blog, CancellationToken cancellationToken) { @@ -53,9 +58,11 @@ public async Task> PostBlog(BlogDto blog, CancellationToke } /// - /// Get posts related to a blog + /// Gets a list of posts related to a specific blog. /// - /// + /// The ID of the blog + /// Cancellation token + /// Returns a list of posts [HttpGet("blog/{blogId:int}/posts", Name = "GetPosts")] public async Task>> GetPosts(int blogId, CancellationToken cancellationToken) { @@ -64,9 +71,11 @@ public async Task>> GetPosts(int blogId, Cance } /// - /// Get specific post by id + /// Gets a specific post by ID. /// - /// + /// The ID of the post + /// Cancellation token + /// Returns the requested post [HttpGet("post/{id:int}", Name = "GetPost")] public async Task> GetPost(int id, CancellationToken cancellationToken) { @@ -79,9 +88,11 @@ public async Task> GetPost(int id, CancellationToken cance } /// - /// Create or update post + /// Creates a new post. /// - /// + /// The post data transfer object + /// Cancellation token + /// Returns the created post [HttpPost("post", Name = "PostPost")] public async Task> PostPost(PostDto post, CancellationToken cancellationToken) { diff --git a/src/backend/WebApi/swagger.json b/src/backend/WebApi/swagger.json index 2ff2446e..b6cd99c3 100644 --- a/src/backend/WebApi/swagger.json +++ b/src/backend/WebApi/swagger.json @@ -20,7 +20,7 @@ "tags": [ "Blogging" ], - "summary": "Get blogs", + "summary": "Gets a list of all blogs.", "operationId": "GetBlogs", "responses": { "200": { @@ -45,9 +45,10 @@ "tags": [ "Blogging" ], - "summary": "Create or update blog", + "summary": "Creates a new blog or updates an existing one.", "operationId": "PostBlog", "requestBody": { + "description": "The blog data transfer object", "content": { "application/json": { "schema": { @@ -88,17 +89,19 @@ "tags": [ "Blogging" ], - "summary": "Get specific blog", + "summary": "Gets a specific blog by ID.", "operationId": "GetBlog", "parameters": [ { "name": "id", "in": "path", + "description": "The ID of the blog.", "required": true, "schema": { "type": "integer", "format": "int32" - } + }, + "example": 1 } ], "responses": { @@ -123,17 +126,19 @@ "tags": [ "Blogging" ], - "summary": "Get posts related to a blog", + "summary": "Gets a list of posts related to a specific blog.", "operationId": "GetPosts", "parameters": [ { "name": "blogId", "in": "path", + "description": "The ID of the blog", "required": true, "schema": { "type": "integer", "format": "int32" - } + }, + "example": 1 } ], "responses": { @@ -161,17 +166,19 @@ "tags": [ "Blogging" ], - "summary": "Get specific post by id", + "summary": "Gets a specific post by ID.", "operationId": "GetPost", "parameters": [ { "name": "id", "in": "path", + "description": "The ID of the post", "required": true, "schema": { "type": "integer", "format": "int32" - } + }, + "example": 1 } ], "responses": { @@ -196,9 +203,10 @@ "tags": [ "Blogging" ], - "summary": "Create or update post", + "summary": "Creates a new post.", "operationId": "PostPost", "requestBody": { + "description": "The post data transfer object", "content": { "application/json": { "schema": { @@ -344,12 +352,14 @@ "blogId": { "type": "integer", "description": "Gets or sets the unique identifier for the blog.", - "format": "int32" + "format": "int32", + "example": 1 }, "url": { "type": "string", "description": "Gets or sets the URL of the blog.", - "nullable": true + "nullable": true, + "example": "https://localhost/a-test-blog" } }, "additionalProperties": false, @@ -366,22 +376,26 @@ "postId": { "type": "integer", "description": "Gets or sets the unique identifier for the post.", - "format": "int32" + "format": "int32", + "example": 1 }, "title": { "type": "string", "description": "Gets or sets the title of the post.", - "nullable": true + "nullable": true, + "example": "Bob Loblaw's Law Blog" }, "content": { "type": "string", "description": "Gets or sets the content of the post.", - "nullable": true + "nullable": true, + "example": "Example blog post content" }, "blogId": { "type": "integer", "description": "Gets or sets the unique identifier of the blog to which the post belongs.", - "format": "int32" + "format": "int32", + "example": 1 } }, "additionalProperties": false, From 316fa81f474b201400b052430c96420d29343e80 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 01:30:55 +0200 Subject: [PATCH 04/12] TEST-0003 Update frontend api clients --- .../api/endpoints/blogging/blogging.msw.ts | 241 +++++++++++------- .../src/api/endpoints/blogging/blogging.ts | 226 ++++++++++------ .../send-message/send-message.msw.ts | 25 +- .../endpoints/send-message/send-message.ts | 10 +- .../src/api/endpoints/service/service.msw.ts | 55 +++- .../src/api/endpoints/service/service.ts | 12 +- .../weather-forecast/weather-forecast.msw.ts | 28 +- .../weather-forecast/weather-forecast.ts | 6 +- src/frontend/src/api/models/blog.ts | 20 -- src/frontend/src/api/models/blogDto.ts | 14 + src/frontend/src/api/models/index.ts | 4 +- .../src/api/models/{post.ts => postDto.ts} | 12 +- 12 files changed, 417 insertions(+), 236 deletions(-) delete mode 100644 src/frontend/src/api/models/blog.ts create mode 100644 src/frontend/src/api/models/blogDto.ts rename src/frontend/src/api/models/{post.ts => postDto.ts} (58%) diff --git a/src/frontend/src/api/endpoints/blogging/blogging.msw.ts b/src/frontend/src/api/endpoints/blogging/blogging.msw.ts index a8828128..d5dd4858 100644 --- a/src/frontend/src/api/endpoints/blogging/blogging.msw.ts +++ b/src/frontend/src/api/endpoints/blogging/blogging.msw.ts @@ -1,135 +1,196 @@ //@ts-nocheck import { faker } from '@faker-js/faker' import { HttpResponse, delay, http } from 'msw' +import type { BlogDto, PostDto } from '../../models' -export const getGetBlogsResponseMock = (): Blog[] => +export const getGetBlogsResponseMock = (): BlogDto[] => Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - posts: faker.helpers.arrayElement([ - Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - content: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - postId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - title: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - })), - undefined, - ]), - url: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), + url: faker.helpers.arrayElement([faker.word.sample(), null]), })) -export const getPostBlogResponseMock = (overrideResponse: Partial = {}): Blog => ({ +export const getPostBlogResponseMock = (overrideResponse: Partial = {}): BlogDto => ({ blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - posts: faker.helpers.arrayElement([ - Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ - blog: faker.helpers.arrayElement([ - { - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - posts: faker.helpers.arrayElement([[], undefined]), - url: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - }, - undefined, - ]), - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - content: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - postId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - title: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - })), - undefined, - ]), - url: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), + url: faker.helpers.arrayElement([faker.word.sample(), null]), + ...overrideResponse, +}) + +export const getGetBlogResponseMock = (overrideResponse: Partial = {}): BlogDto => ({ + blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), + url: faker.helpers.arrayElement([faker.word.sample(), null]), ...overrideResponse, }) -export const getGetPostsResponseMock = (): Post[] => +export const getGetPostsResponseMock = (): PostDto[] => Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ - blog: faker.helpers.arrayElement([ - { - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - posts: faker.helpers.arrayElement([[], undefined]), - url: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - }, - undefined, - ]), - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - content: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), + blogId: faker.number.int({ min: undefined, max: undefined }), + content: faker.helpers.arrayElement([faker.word.sample(), null]), postId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - title: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), + title: faker.helpers.arrayElement([faker.word.sample(), null]), })) -export const getPostPostResponseMock = (overrideResponse: Partial = {}): Post => ({ - blog: faker.helpers.arrayElement([ - { - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - posts: faker.helpers.arrayElement([ - Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - content: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - postId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - title: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - })), - undefined, - ]), - url: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), - }, - undefined, - ]), - blogId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - content: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), +export const getGetPostResponseMock = (overrideResponse: Partial = {}): PostDto => ({ + blogId: faker.number.int({ min: undefined, max: undefined }), + content: faker.helpers.arrayElement([faker.word.sample(), null]), postId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), - title: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.word.sample(), null]), undefined]), + title: faker.helpers.arrayElement([faker.word.sample(), null]), ...overrideResponse, }) -export const getGetBlogsMockHandler = () => { - return http.get('*/Blogging/Blog', async () => { +export const getPostPostResponseMock = (overrideResponse: Partial = {}): PostDto => ({ + blogId: faker.number.int({ min: undefined, max: undefined }), + content: faker.helpers.arrayElement([faker.word.sample(), null]), + postId: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), + title: faker.helpers.arrayElement([faker.word.sample(), null]), + ...overrideResponse, +}) + +export const getGetBlogsMockHandler = ( + overrideResponse?: + | BlogDto[] + | ((info: Parameters[1]>[0]) => Promise | BlogDto[]), +) => { + return http.get('*/Blogging/blog', async (info) => { await delay(1000) - return new HttpResponse(getGetBlogsResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetBlogsResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } -export const getPostBlogMockHandler = () => { - return http.post('*/Blogging/Blog', async () => { +export const getPostBlogMockHandler = ( + overrideResponse?: BlogDto | ((info: Parameters[1]>[0]) => Promise | BlogDto), +) => { + return http.post('*/Blogging/blog', async (info) => { await delay(1000) - return new HttpResponse(getPostBlogResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getPostBlogResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } -export const getGetPostsMockHandler = () => { - return http.get('*/Blogging/Post', async () => { +export const getGetBlogMockHandler = ( + overrideResponse?: BlogDto | ((info: Parameters[1]>[0]) => Promise | BlogDto), +) => { + return http.get('*/Blogging/blog/:id', async (info) => { await delay(1000) - return new HttpResponse(getGetPostsResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetBlogResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } -export const getPostPostMockHandler = () => { - return http.post('*/Blogging/Post', async () => { +export const getGetPostsMockHandler = ( + overrideResponse?: + | PostDto[] + | ((info: Parameters[1]>[0]) => Promise | PostDto[]), +) => { + return http.get('*/Blogging/blog/:blogId/posts', async (info) => { await delay(1000) - return new HttpResponse(getPostPostResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetPostsResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) +} + +export const getGetPostMockHandler = ( + overrideResponse?: PostDto | ((info: Parameters[1]>[0]) => Promise | PostDto), +) => { + return http.get('*/Blogging/post/:id', async (info) => { + await delay(1000) + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetPostResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) +} + +export const getPostPostMockHandler = ( + overrideResponse?: PostDto | ((info: Parameters[1]>[0]) => Promise | PostDto), +) => { + return http.post('*/Blogging/post', async (info) => { + await delay(1000) + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getPostPostResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } export const getBloggingMock = () => [ getGetBlogsMockHandler(), getPostBlogMockHandler(), + getGetBlogMockHandler(), getGetPostsMockHandler(), + getGetPostMockHandler(), getPostPostMockHandler(), ] diff --git a/src/frontend/src/api/endpoints/blogging/blogging.ts b/src/frontend/src/api/endpoints/blogging/blogging.ts index 4702782c..2da1442a 100644 --- a/src/frontend/src/api/endpoints/blogging/blogging.ts +++ b/src/frontend/src/api/endpoints/blogging/blogging.ts @@ -9,41 +9,24 @@ import type { UseQueryOptions, UseQueryResult, } from '@tanstack/react-query' -import type { Blog, Post } from '../../models' +import type { BlogDto, PostDto } from '../../models' import { customInstance } from '../../mutators/custom-instance' import type { ErrorType } from '../../mutators/custom-instance' -// https://stackoverflow.com/questions/49579094/typescript-conditional-types-filter-out-readonly-properties-pick-only-requir/49579497#49579497 -type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B - -type WritableKeys = { - [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P> -}[keyof T] - -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never -type DistributeReadOnlyOverUnions = T extends any ? NonReadonly : never - -type Writable = Pick> -type NonReadonly = [T] extends [UnionToIntersection] - ? { - [P in keyof Writable]: T[P] extends object ? NonReadonly> : T[P] - } - : DistributeReadOnlyOverUnions - /** - * @summary Get blogs + * @summary Gets a list of all blogs. */ export const getBlogs = (signal?: AbortSignal) => { - return customInstance({ url: `/Blogging/Blog`, method: 'GET', signal }) + return customInstance({ url: `/Blogging/blog`, method: 'GET', signal }) } export const getGetBlogsQueryKey = () => { - return [`/Blogging/Blog`] as const + return [`/Blogging/blog`] as const } export const getGetBlogsQueryOptions = < TData = Awaited>, - TError = ErrorType, + TError = ErrorType, >(options?: { query?: Partial>, TError, TData>> }) => { @@ -61,12 +44,12 @@ export const getGetBlogsQueryOptions = < } export type GetBlogsQueryResult = NonNullable>> -export type GetBlogsQueryError = ErrorType +export type GetBlogsQueryError = ErrorType /** - * @summary Get blogs + * @summary Gets a list of all blogs. */ -export const useGetBlogs = >, TError = ErrorType>(options?: { +export const useGetBlogs = >, TError = ErrorType>(options?: { query?: Partial>, TError, TData>> }): UseQueryResult & { queryKey: QueryKey } => { const queryOptions = getGetBlogsQueryOptions(options) @@ -79,23 +62,23 @@ export const useGetBlogs = >, TError } /** - * @summary Create blog + * @summary Creates a new blog or updates an existing one. */ -export const postBlog = (blog: NonReadonly) => { - return customInstance({ - url: `/Blogging/Blog`, +export const postBlog = (blogDto: BlogDto) => { + return customInstance({ + url: `/Blogging/blog`, method: 'POST', headers: { 'Content-Type': 'application/json' }, - data: blog, + data: blogDto, }) } -export const getPostBlogMutationOptions = , TContext = unknown>(options?: { - mutation?: UseMutationOptions>, TError, { data: NonReadonly }, TContext> -}): UseMutationOptions>, TError, { data: NonReadonly }, TContext> => { +export const getPostBlogMutationOptions = , TContext = unknown>(options?: { + mutation?: UseMutationOptions>, TError, { data: BlogDto }, TContext> +}): UseMutationOptions>, TError, { data: BlogDto }, TContext> => { const { mutation: mutationOptions } = options ?? {} - const mutationFn: MutationFunction>, { data: NonReadonly }> = (props) => { + const mutationFn: MutationFunction>, { data: BlogDto }> = (props) => { const { data } = props ?? {} return postBlog(data) @@ -105,43 +88,88 @@ export const getPostBlogMutationOptions = , TContext } export type PostBlogMutationResult = NonNullable>> -export type PostBlogMutationBody = NonReadonly -export type PostBlogMutationError = ErrorType +export type PostBlogMutationBody = BlogDto +export type PostBlogMutationError = ErrorType /** - * @summary Create blog + * @summary Creates a new blog or updates an existing one. */ -export const usePostBlog = , TContext = unknown>(options?: { - mutation?: UseMutationOptions>, TError, { data: NonReadonly }, TContext> -}): UseMutationResult>, TError, { data: NonReadonly }, TContext> => { +export const usePostBlog = , TContext = unknown>(options?: { + mutation?: UseMutationOptions>, TError, { data: BlogDto }, TContext> +}): UseMutationResult>, TError, { data: BlogDto }, TContext> => { const mutationOptions = getPostBlogMutationOptions(options) return useMutation(mutationOptions) } /** - * @summary Get posts + * @summary Gets a specific blog by ID. */ -export const getPosts = (signal?: AbortSignal) => { - return customInstance({ url: `/Blogging/Post`, method: 'GET', signal }) +export const getBlog = (id: number, signal?: AbortSignal) => { + return customInstance({ url: `/Blogging/blog/${id}`, method: 'GET', signal }) } -export const getGetPostsQueryKey = () => { - return [`/Blogging/Post`] as const +export const getGetBlogQueryKey = (id: number) => { + return [`/Blogging/blog/${id}`] as const } -export const getGetPostsQueryOptions = < - TData = Awaited>, - TError = ErrorType, ->(options?: { - query?: Partial>, TError, TData>> -}) => { +export const getGetBlogQueryOptions = >, TError = ErrorType>( + id: number, + options?: { query?: Partial>, TError, TData>> }, +) => { const { query: queryOptions } = options ?? {} - const queryKey = queryOptions?.queryKey ?? getGetPostsQueryKey() + const queryKey = queryOptions?.queryKey ?? getGetBlogQueryKey(id) - const queryFn: QueryFunction>> = ({ signal }) => getPosts(signal) + const queryFn: QueryFunction>> = ({ signal }) => getBlog(id, signal) - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + return { queryKey, queryFn, enabled: !!id, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey } +} + +export type GetBlogQueryResult = NonNullable>> +export type GetBlogQueryError = ErrorType + +/** + * @summary Gets a specific blog by ID. + */ +export const useGetBlog = >, TError = ErrorType>( + id: number, + options?: { query?: Partial>, TError, TData>> }, +): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetBlogQueryOptions(id, options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey } + + query.queryKey = queryOptions.queryKey + + return query +} + +/** + * @summary Gets a list of posts related to a specific blog. + */ +export const getPosts = (blogId: number, signal?: AbortSignal) => { + return customInstance({ url: `/Blogging/blog/${blogId}/posts`, method: 'GET', signal }) +} + +export const getGetPostsQueryKey = (blogId: number) => { + return [`/Blogging/blog/${blogId}/posts`] as const +} + +export const getGetPostsQueryOptions = >, TError = ErrorType>( + blogId: number, + options?: { query?: Partial>, TError, TData>> }, +) => { + const { query: queryOptions } = options ?? {} + + const queryKey = queryOptions?.queryKey ?? getGetPostsQueryKey(blogId) + + const queryFn: QueryFunction>> = ({ signal }) => getPosts(blogId, signal) + + return { queryKey, queryFn, enabled: !!blogId, ...queryOptions } as UseQueryOptions< Awaited>, TError, TData @@ -149,15 +177,63 @@ export const getGetPostsQueryOptions = < } export type GetPostsQueryResult = NonNullable>> -export type GetPostsQueryError = ErrorType +export type GetPostsQueryError = ErrorType /** - * @summary Get posts + * @summary Gets a list of posts related to a specific blog. */ -export const useGetPosts = >, TError = ErrorType>(options?: { - query?: Partial>, TError, TData>> -}): UseQueryResult & { queryKey: QueryKey } => { - const queryOptions = getGetPostsQueryOptions(options) +export const useGetPosts = >, TError = ErrorType>( + blogId: number, + options?: { query?: Partial>, TError, TData>> }, +): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetPostsQueryOptions(blogId, options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey } + + query.queryKey = queryOptions.queryKey + + return query +} + +/** + * @summary Gets a specific post by ID. + */ +export const getPost = (id: number, signal?: AbortSignal) => { + return customInstance({ url: `/Blogging/post/${id}`, method: 'GET', signal }) +} + +export const getGetPostQueryKey = (id: number) => { + return [`/Blogging/post/${id}`] as const +} + +export const getGetPostQueryOptions = >, TError = ErrorType>( + id: number, + options?: { query?: Partial>, TError, TData>> }, +) => { + const { query: queryOptions } = options ?? {} + + const queryKey = queryOptions?.queryKey ?? getGetPostQueryKey(id) + + const queryFn: QueryFunction>> = ({ signal }) => getPost(id, signal) + + return { queryKey, queryFn, enabled: !!id, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey } +} + +export type GetPostQueryResult = NonNullable>> +export type GetPostQueryError = ErrorType + +/** + * @summary Gets a specific post by ID. + */ +export const useGetPost = >, TError = ErrorType>( + id: number, + options?: { query?: Partial>, TError, TData>> }, +): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetPostQueryOptions(id, options) const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey } @@ -167,23 +243,23 @@ export const useGetPosts = >, TError } /** - * @summary Create post + * @summary Creates a new post. */ -export const postPost = (post: Post) => { - return customInstance({ - url: `/Blogging/Post`, +export const postPost = (postDto: PostDto) => { + return customInstance({ + url: `/Blogging/post`, method: 'POST', headers: { 'Content-Type': 'application/json' }, - data: post, + data: postDto, }) } -export const getPostPostMutationOptions = , TContext = unknown>(options?: { - mutation?: UseMutationOptions>, TError, { data: Post }, TContext> -}): UseMutationOptions>, TError, { data: Post }, TContext> => { +export const getPostPostMutationOptions = , TContext = unknown>(options?: { + mutation?: UseMutationOptions>, TError, { data: PostDto }, TContext> +}): UseMutationOptions>, TError, { data: PostDto }, TContext> => { const { mutation: mutationOptions } = options ?? {} - const mutationFn: MutationFunction>, { data: Post }> = (props) => { + const mutationFn: MutationFunction>, { data: PostDto }> = (props) => { const { data } = props ?? {} return postPost(data) @@ -193,15 +269,15 @@ export const getPostPostMutationOptions = , TContext } export type PostPostMutationResult = NonNullable>> -export type PostPostMutationBody = Post -export type PostPostMutationError = ErrorType +export type PostPostMutationBody = PostDto +export type PostPostMutationError = ErrorType /** - * @summary Create post + * @summary Creates a new post. */ -export const usePostPost = , TContext = unknown>(options?: { - mutation?: UseMutationOptions>, TError, { data: Post }, TContext> -}): UseMutationResult>, TError, { data: Post }, TContext> => { +export const usePostPost = , TContext = unknown>(options?: { + mutation?: UseMutationOptions>, TError, { data: PostDto }, TContext> +}): UseMutationResult>, TError, { data: PostDto }, TContext> => { const mutationOptions = getPostPostMutationOptions(options) return useMutation(mutationOptions) diff --git a/src/frontend/src/api/endpoints/send-message/send-message.msw.ts b/src/frontend/src/api/endpoints/send-message/send-message.msw.ts index 0f33ffc6..88da0236 100644 --- a/src/frontend/src/api/endpoints/send-message/send-message.msw.ts +++ b/src/frontend/src/api/endpoints/send-message/send-message.msw.ts @@ -4,15 +4,26 @@ import { HttpResponse, delay, http } from 'msw' export const getGetSendMessageResponseMock = (): string => faker.word.sample() -export const getGetSendMessageMockHandler = () => { - return http.get('*/SendMessage', async () => { +export const getGetSendMessageMockHandler = ( + overrideResponse?: string | ((info: Parameters[1]>[0]) => Promise | string), +) => { + return http.get('*/SendMessage', async (info) => { await delay(1000) - return new HttpResponse(getGetSendMessageResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetSendMessageResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } export const getSendMessageMock = () => [getGetSendMessageMockHandler()] diff --git a/src/frontend/src/api/endpoints/send-message/send-message.ts b/src/frontend/src/api/endpoints/send-message/send-message.ts index feb89f11..11a83056 100644 --- a/src/frontend/src/api/endpoints/send-message/send-message.ts +++ b/src/frontend/src/api/endpoints/send-message/send-message.ts @@ -5,7 +5,7 @@ import { customInstance } from '../../mutators/custom-instance' import type { ErrorType } from '../../mutators/custom-instance' /** - * @summary TODO + * @summary Sends a message using the MessageSender service. */ export const getSendMessage = (signal?: AbortSignal) => { return customInstance({ url: `/SendMessage`, method: 'GET', signal }) @@ -17,7 +17,7 @@ export const getGetSendMessageQueryKey = () => { export const getGetSendMessageQueryOptions = < TData = Awaited>, - TError = ErrorType, + TError = ErrorType, >(options?: { query?: Partial>, TError, TData>> }) => { @@ -35,14 +35,14 @@ export const getGetSendMessageQueryOptions = < } export type GetSendMessageQueryResult = NonNullable>> -export type GetSendMessageQueryError = ErrorType +export type GetSendMessageQueryError = ErrorType /** - * @summary TODO + * @summary Sends a message using the MessageSender service. */ export const useGetSendMessage = < TData = Awaited>, - TError = ErrorType, + TError = ErrorType, >(options?: { query?: Partial>, TError, TData>> }): UseQueryResult & { queryKey: QueryKey } => { diff --git a/src/frontend/src/api/endpoints/service/service.msw.ts b/src/frontend/src/api/endpoints/service/service.msw.ts index 5c76753f..c89b7247 100644 --- a/src/frontend/src/api/endpoints/service/service.msw.ts +++ b/src/frontend/src/api/endpoints/service/service.msw.ts @@ -1,6 +1,7 @@ //@ts-nocheck import { faker } from '@faker-js/faker' import { HttpResponse, delay, http } from 'msw' +import type { StringGenericValue, VersionInformation } from '../../models' export const getPingResponseMock = (overrideResponse: Partial = {}): StringGenericValue => ({ value: faker.helpers.arrayElement([faker.word.sample(), null]), @@ -21,27 +22,53 @@ export const getVersionResponseMock = (overrideResponse: Partial { - return http.get('*/Service/ping', async () => { +export const getPingMockHandler = ( + overrideResponse?: + | StringGenericValue + | ((info: Parameters[1]>[0]) => Promise | StringGenericValue), +) => { + return http.get('*/Service/ping', async (info) => { await delay(1000) - return new HttpResponse(getPingResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getPingResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } -export const getVersionMockHandler = () => { - return http.get('*/Service/version', async () => { +export const getVersionMockHandler = ( + overrideResponse?: + | VersionInformation + | ((info: Parameters[1]>[0]) => Promise | VersionInformation), +) => { + return http.get('*/Service/version', async (info) => { await delay(1000) - return new HttpResponse(getVersionResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getVersionResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } export const getServiceMock = () => [getPingMockHandler(), getVersionMockHandler()] diff --git a/src/frontend/src/api/endpoints/service/service.ts b/src/frontend/src/api/endpoints/service/service.ts index 70cff9d9..993196c0 100644 --- a/src/frontend/src/api/endpoints/service/service.ts +++ b/src/frontend/src/api/endpoints/service/service.ts @@ -16,7 +16,7 @@ export const getPingQueryKey = () => { return [`/Service/ping`] as const } -export const getPingQueryOptions = >, TError = ErrorType>(options?: { +export const getPingQueryOptions = >, TError = ErrorType>(options?: { query?: Partial>, TError, TData>> }) => { const { query: queryOptions } = options ?? {} @@ -31,12 +31,12 @@ export const getPingQueryOptions = >, TE } export type PingQueryResult = NonNullable>> -export type PingQueryError = ErrorType +export type PingQueryError = ErrorType /** * @summary Returns ok */ -export const usePing = >, TError = ErrorType>(options?: { +export const usePing = >, TError = ErrorType>(options?: { query?: Partial>, TError, TData>> }): UseQueryResult & { queryKey: QueryKey } => { const queryOptions = getPingQueryOptions(options) @@ -61,7 +61,7 @@ export const getVersionQueryKey = () => { export const getVersionQueryOptions = < TData = Awaited>, - TError = ErrorType, + TError = ErrorType, >(options?: { query?: Partial>, TError, TData>> }) => { @@ -79,12 +79,12 @@ export const getVersionQueryOptions = < } export type VersionQueryResult = NonNullable>> -export type VersionQueryError = ErrorType +export type VersionQueryError = ErrorType /** * @summary Returns version */ -export const useVersion = >, TError = ErrorType>(options?: { +export const useVersion = >, TError = ErrorType>(options?: { query?: Partial>, TError, TData>> }): UseQueryResult & { queryKey: QueryKey } => { const queryOptions = getVersionQueryOptions(options) diff --git a/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.msw.ts b/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.msw.ts index 00bc0ad3..1bbf1a10 100644 --- a/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.msw.ts +++ b/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.msw.ts @@ -1,6 +1,7 @@ //@ts-nocheck import { faker } from '@faker-js/faker' import { HttpResponse, delay, http } from 'msw' +import type { WeatherForecast } from '../../models' export const getGetWeatherForecastResponseMock = (): WeatherForecast[] => Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ @@ -9,15 +10,28 @@ export const getGetWeatherForecastResponseMock = (): WeatherForecast[] => temperatureC: faker.helpers.arrayElement([faker.number.int({ min: undefined, max: undefined }), undefined]), })) -export const getGetWeatherForecastMockHandler = () => { - return http.get('*/WeatherForecast', async () => { +export const getGetWeatherForecastMockHandler = ( + overrideResponse?: + | WeatherForecast[] + | ((info: Parameters[1]>[0]) => Promise | WeatherForecast[]), +) => { + return http.get('*/WeatherForecast', async (info) => { await delay(1000) - return new HttpResponse(getGetWeatherForecastResponseMock(), { - status: 200, - headers: { - 'Content-Type': 'text/plain', + return new HttpResponse( + JSON.stringify( + overrideResponse !== undefined + ? typeof overrideResponse === 'function' + ? await overrideResponse(info) + : overrideResponse + : getGetWeatherForecastResponseMock(), + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, }, - }) + ) }) } export const getWeatherForecastMock = () => [getGetWeatherForecastMockHandler()] diff --git a/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.ts b/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.ts index d3cfc0ae..fe164be5 100644 --- a/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.ts +++ b/src/frontend/src/api/endpoints/weather-forecast/weather-forecast.ts @@ -18,7 +18,7 @@ export const getGetWeatherForecastQueryKey = () => { export const getGetWeatherForecastQueryOptions = < TData = Awaited>, - TError = ErrorType, + TError = ErrorType, >(options?: { query?: Partial>, TError, TData>> }) => { @@ -37,14 +37,14 @@ export const getGetWeatherForecastQueryOptions = < } export type GetWeatherForecastQueryResult = NonNullable>> -export type GetWeatherForecastQueryError = ErrorType +export type GetWeatherForecastQueryError = ErrorType /** * @summary Returns weather forecast */ export const useGetWeatherForecast = < TData = Awaited>, - TError = ErrorType, + TError = ErrorType, >(options?: { query?: Partial>, TError, TData>> }): UseQueryResult & { queryKey: QueryKey } => { diff --git a/src/frontend/src/api/models/blog.ts b/src/frontend/src/api/models/blog.ts deleted file mode 100644 index 471447e2..00000000 --- a/src/frontend/src/api/models/blog.ts +++ /dev/null @@ -1,20 +0,0 @@ -//@ts-nocheck -import type { Post } from './post' - -/** - * Represents a blog with a unique ID, URL, and a collection of posts. - */ -export interface Blog { - /** Gets or sets the unique identifier for the blog. */ - blogId?: number - /** - * Gets the collection of posts associated with the blog. - * @nullable - */ - readonly posts?: readonly Post[] | null - /** - * Gets or sets the URL of the blog. - * @nullable - */ - url?: string | null -} diff --git a/src/frontend/src/api/models/blogDto.ts b/src/frontend/src/api/models/blogDto.ts new file mode 100644 index 00000000..c8273ec8 --- /dev/null +++ b/src/frontend/src/api/models/blogDto.ts @@ -0,0 +1,14 @@ +//@ts-nocheck + +/** + * Blog DTO + */ +export interface BlogDto { + /** Gets or sets the unique identifier for the blog. */ + blogId?: number + /** + * Gets or sets the URL of the blog. + * @nullable + */ + url: string | null +} diff --git a/src/frontend/src/api/models/index.ts b/src/frontend/src/api/models/index.ts index 3e6088d0..2dcf1832 100644 --- a/src/frontend/src/api/models/index.ts +++ b/src/frontend/src/api/models/index.ts @@ -1,7 +1,7 @@ //@ts-nocheck -export * from './blog' -export * from './post' +export * from './blogDto' +export * from './postDto' export * from './stringGenericValue' export * from './versionInformation' export * from './weatherForecast' diff --git a/src/frontend/src/api/models/post.ts b/src/frontend/src/api/models/postDto.ts similarity index 58% rename from src/frontend/src/api/models/post.ts rename to src/frontend/src/api/models/postDto.ts index db1eed21..bcf62e95 100644 --- a/src/frontend/src/api/models/post.ts +++ b/src/frontend/src/api/models/postDto.ts @@ -1,23 +1,21 @@ //@ts-nocheck -import type { Blog } from './blog' /** - * Represents a post with a unique ID, title, content, and the associated blog ID. + * Post DTO */ -export interface Post { - blog?: Blog +export interface PostDto { /** Gets or sets the unique identifier of the blog to which the post belongs. */ - blogId?: number + blogId: number /** * Gets or sets the content of the post. * @nullable */ - content?: string | null + content: string | null /** Gets or sets the unique identifier for the post. */ postId?: number /** * Gets or sets the title of the post. * @nullable */ - title?: string | null + title: string | null } From f6105343f76d34939fc46b2c920f9b582a98c4a6 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 14:38:24 +0200 Subject: [PATCH 05/12] TEST-0003 Fix tags should have postfix --- .github/actions/frontend-build/action.yml | 2 +- .github/workflows/backend-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/frontend-build/action.yml b/.github/actions/frontend-build/action.yml index fc110cb7..fd1293af 100644 --- a/.github/actions/frontend-build/action.yml +++ b/.github/actions/frontend-build/action.yml @@ -147,4 +147,4 @@ runs: with: github-token: ${{ env.GH_TOKEN }} review-message: "Please take a look :eyes:" - tags: "TODO,FIXME,BUG" + tags: "TODO:,FIXME:,BUG:" diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 81eeebd1..81c7cd0e 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -119,7 +119,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} review-message: "Please take a look :eyes:" - tags: "TODO,FIXME,BUG" + tags: "TODO:,FIXME:,BUG:" # # TODO: move? # - name: Super-linter From b621ce5d0520485c57a6aae16814dfcb129a9d45 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 14:39:03 +0200 Subject: [PATCH 06/12] TEST-0003 Add title to Blog model and test cases --- .../WebApi/Database/BloggingContext.cs | 12 +++ src/backend/WebApi/Entities/BlogDto.cs | 10 ++ ...240623122654_AddBlogTitleField.Designer.cs | 96 +++++++++++++++++++ .../20240623122654_AddBlogTitleField.cs | 30 ++++++ .../BloggingContextModelSnapshot.cs | 15 ++- .../WebApi/Repository/BloggingRepository.cs | 3 +- src/backend/WebApi/swagger.json | 7 ++ .../Controllers/BloggingControllerTests.cs | 89 +++++++++++++++++ 8 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.Designer.cs create mode 100644 src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.cs create mode 100644 tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs diff --git a/src/backend/WebApi/Database/BloggingContext.cs b/src/backend/WebApi/Database/BloggingContext.cs index 44d167c6..a3783c18 100644 --- a/src/backend/WebApi/Database/BloggingContext.cs +++ b/src/backend/WebApi/Database/BloggingContext.cs @@ -34,6 +34,12 @@ public interface IBlog /// 1 public int BlogId { get; set; } + /// + /// Gets or sets the title of the blog. + /// + /// My blog title + public string Title { get; set; } + /// /// Gets or sets the URL of the blog. /// @@ -53,6 +59,9 @@ public class Blog : IBlog /// public int BlogId { get; set; } + /// + public required string Title { get; set; } + /// public required string Url { get; set; } } @@ -65,6 +74,9 @@ public class BlogConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { + builder.Property(static post => post.Title) + .HasMaxLength(500); + builder.Property(static post => post.Url) .HasMaxLength(1000); } diff --git a/src/backend/WebApi/Entities/BlogDto.cs b/src/backend/WebApi/Entities/BlogDto.cs index e4f62eff..19526ef7 100644 --- a/src/backend/WebApi/Entities/BlogDto.cs +++ b/src/backend/WebApi/Entities/BlogDto.cs @@ -11,12 +11,21 @@ public record BlogDto : IBlog, IValidatableObject /// public int BlogId { get; set; } + /// + public required string Title { get; set; } + /// public required string Url { get; set; } /// public IEnumerable Validate(ValidationContext validationContext) { + if (string.IsNullOrEmpty(Title)) + yield return new ValidationResult($"{nameof(Title)} must be set"); + + if (Title.Length > 1000) + yield return new ValidationResult($"{nameof(Title)} is longer than the maximum amount of characters (500)"); + if (string.IsNullOrEmpty(Url)) yield return new ValidationResult($"{nameof(Url)} must be set"); @@ -40,6 +49,7 @@ public static BlogDto FromEntity(Blog blog) => new() { BlogId = blog.BlogId, + Title = blog.Title, Url = blog.Url }; } \ No newline at end of file diff --git a/src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.Designer.cs b/src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.Designer.cs new file mode 100644 index 00000000..35832f15 --- /dev/null +++ b/src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.Designer.cs @@ -0,0 +1,96 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WebApi.Database; + +#nullable disable + +namespace WebApi.Migrations +{ + [DbContext(typeof(BloggingContext))] + [Migration("20240623122654_AddBlogTitleField")] + partial class AddBlogTitleField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("WebApi.Database.Blog", b => + { + b.Property("BlogId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BlogId")); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("BlogId"); + + b.ToTable("Blogs"); + }); + + modelBuilder.Entity("WebApi.Database.Post", b => + { + b.Property("PostId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PostId")); + + b.Property("BlogId") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("PostId"); + + b.HasIndex("BlogId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("WebApi.Database.Post", b => + { + b.HasOne("WebApi.Database.Blog", "Blog") + .WithMany("Posts") + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Blog"); + }); + + modelBuilder.Entity("WebApi.Database.Blog", b => + { + b.Navigation("Posts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.cs b/src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.cs new file mode 100644 index 00000000..b8a67e95 --- /dev/null +++ b/src/backend/WebApi/Migrations/20240623122654_AddBlogTitleField.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WebApi.Migrations +{ + /// + public partial class AddBlogTitleField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Title", + table: "Blogs", + type: "nvarchar(500)", + maxLength: 500, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Title", + table: "Blogs"); + } + } +} diff --git a/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs b/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs index 6b6cb013..f39c9031 100644 --- a/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs +++ b/src/backend/WebApi/Migrations/BloggingContextModelSnapshot.cs @@ -21,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("WebApi.Blog", b => + modelBuilder.Entity("WebApi.Database.Blog", b => { b.Property("BlogId") .ValueGeneratedOnAdd() @@ -29,6 +29,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BlogId")); + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + b.Property("Url") .IsRequired() .HasMaxLength(1000) @@ -39,7 +44,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Blogs"); }); - modelBuilder.Entity("WebApi.Post", b => + modelBuilder.Entity("WebApi.Database.Post", b => { b.Property("PostId") .ValueGeneratedOnAdd() @@ -67,9 +72,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Posts"); }); - modelBuilder.Entity("WebApi.Post", b => + modelBuilder.Entity("WebApi.Database.Post", b => { - b.HasOne("WebApi.Blog", "Blog") + b.HasOne("WebApi.Database.Blog", "Blog") .WithMany("Posts") .HasForeignKey("BlogId") .OnDelete(DeleteBehavior.Cascade) @@ -78,7 +83,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Blog"); }); - modelBuilder.Entity("WebApi.Blog", b => + modelBuilder.Entity("WebApi.Database.Blog", b => { b.Navigation("Posts"); }); diff --git a/src/backend/WebApi/Repository/BloggingRepository.cs b/src/backend/WebApi/Repository/BloggingRepository.cs index 4a9633b2..b1b941e3 100644 --- a/src/backend/WebApi/Repository/BloggingRepository.cs +++ b/src/backend/WebApi/Repository/BloggingRepository.cs @@ -113,7 +113,8 @@ public class BloggingRepository(BloggingContext bloggingContext) : IBloggingRepo // Add var createdBlogEntity = bloggingContext.Blogs.Add(new Blog { - Url = blog.Url + Url = blog.Url, + Title = blog.Title }); await bloggingContext.SaveChangesAsync(cancellationToken); diff --git a/src/backend/WebApi/swagger.json b/src/backend/WebApi/swagger.json index b6cd99c3..d02c8ec8 100644 --- a/src/backend/WebApi/swagger.json +++ b/src/backend/WebApi/swagger.json @@ -345,6 +345,7 @@ "schemas": { "BlogDto": { "required": [ + "title", "url" ], "type": "object", @@ -355,6 +356,12 @@ "format": "int32", "example": 1 }, + "title": { + "type": "string", + "description": "Gets or sets the title of the blog.", + "nullable": true, + "example": "My blog title" + }, "url": { "type": "string", "description": "Gets or sets the URL of the blog.", diff --git a/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs b/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs new file mode 100644 index 00000000..765a2fc9 --- /dev/null +++ b/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using WebApi.Controllers; +using WebApi.Entities; +using WebApi.Repository; + +namespace WebApi.Tests.Controllers; + +[TestFixture] +public class BloggingControllerTests +{ + [SetUp] + public void SetUp() + { + _loggerMock = new Mock>(); + _bloggingRepositoryMock = new Mock(); + _controller = new BloggingController(_loggerMock.Object, _bloggingRepositoryMock.Object); + } + private Mock> _loggerMock; + private Mock _bloggingRepositoryMock; + private BloggingController _controller; + + [Test] + public async Task GetBlogs_ReturnsListOfBlogs() + { + // Arrange + var blogs = new List + { + new() + { + Url = "url1", + Title = "title1" + }, + new() + { + Url = "url2", + Title = "title2" + } + }; + + _bloggingRepositoryMock.Setup(static repo => repo.ListBlogsAsync(It.IsAny())) + .ReturnsAsync(blogs); + + // Act + var result = await _controller.GetBlogs(CancellationToken.None); + + // Assert + Assert.That(result, Is.InstanceOf>>()); + var okResult = result.Result as OkObjectResult; + Assert.That(okResult, Is.Not.Null); + var returnedBlogs = okResult.Value as IEnumerable; + Assert.That(returnedBlogs, Is.Not.Null); + Assert.That(returnedBlogs.Count(), Is.EqualTo(2)); + } + + [Test] + public async Task GetBlog_ValidId_ReturnsBlog() + { + // Arrange + var blog = new BlogDto + { + BlogId = 1, + Title = "Test Blog", + Url = "test://url" + }; + + _bloggingRepositoryMock.Setup(static repo => repo.GetBlogAsync(1, It.IsAny())) + .ReturnsAsync(blog); + + // Act + var result = await _controller.GetBlog(1, CancellationToken.None); + + // Assert + Assert.That(result, Is.InstanceOf>()); + var okResult = result.Result as OkObjectResult; + Assert.That(okResult, Is.Not.Null); + var returnedBlog = okResult.Value as BlogDto; + Assert.That(returnedBlog, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(returnedBlog.BlogId, Is.EqualTo(blog.BlogId)); + Assert.That(returnedBlog.Title, Is.EqualTo(blog.Title)); + Assert.That(returnedBlog.Url, Is.EqualTo(blog.Url)); + }); + } + + // TODO: Implement tests for the remaining actions using a similar pattern. +} \ No newline at end of file From 5511aad3452dd3aa4a0c20e7d69aaca0a8d5ee5a Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 18:00:50 +0200 Subject: [PATCH 07/12] TEST-0003 Add continue on error --- .github/workflows/backend-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 81c7cd0e..5d7c26e1 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -137,6 +137,7 @@ jobs: - name: WebApi Tests run: dotnet test ../../tests/backend/WebApi.Tests --no-restore --collect:"XPlat Code Coverage" --logger "trx;LogFileName=test-results.trx" /p:CollectCoverage=true /p:CoverletOutput="../../tests/backend/WebApi.Tests/TestResults/" /p:CoverletOutputFormat=cobertura + continue-on-error: true - uses: dorny/test-reporter@v1 with: @@ -203,5 +204,4 @@ jobs: solutionWideAnalysis: true include: | **.cs - ignoreIssueType: - PropertyCanBeMadeInitOnly.Global + ignoreIssueType: PropertyCanBeMadeInitOnly.Global From bf0126612a61b41eb36b1750fc0af723d63541d3 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 18:03:13 +0200 Subject: [PATCH 08/12] TEST-0003 Remove fail on error for test reporter --- .github/workflows/backend-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 5d7c26e1..5091d2ce 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -144,7 +144,6 @@ jobs: name: "WebApi Tests" path: "tests/backend/WebApi.Tests/TestResults/test-results.trx" reporter: dotnet-trx - fail-on-error: true - name: Generate report for all tests id: reportgenerator From 5e7856d4b6c14f35ffd178537e8844d6baf089c1 Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sun, 23 Jun 2024 18:06:26 +0200 Subject: [PATCH 09/12] TEST-0003 Change continue on error to explicit false --- .github/workflows/backend-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 5091d2ce..b26e83ed 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -144,6 +144,7 @@ jobs: name: "WebApi Tests" path: "tests/backend/WebApi.Tests/TestResults/test-results.trx" reporter: dotnet-trx + fail-on-error: false - name: Generate report for all tests id: reportgenerator From 0ede668bcb3b7a032972717ee49e6505ff1dcbeb Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Mon, 24 Jun 2024 01:06:59 +0200 Subject: [PATCH 10/12] TEST-0003 Fix nunit support temporarily via forked build of action --- .github/workflows/backend-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index b26e83ed..ae9cfdd7 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -139,11 +139,12 @@ jobs: run: dotnet test ../../tests/backend/WebApi.Tests --no-restore --collect:"XPlat Code Coverage" --logger "trx;LogFileName=test-results.trx" /p:CollectCoverage=true /p:CoverletOutput="../../tests/backend/WebApi.Tests/TestResults/" /p:CoverletOutputFormat=cobertura continue-on-error: true - - uses: dorny/test-reporter@v1 + - uses: hwinther/test-reporter@dotnet-nunit # dorny/test-reporter@v1 + id: test-reporter with: name: "WebApi Tests" path: "tests/backend/WebApi.Tests/TestResults/test-results.trx" - reporter: dotnet-trx + reporter: dotnet-trx # TODO: nunit is not supported fail-on-error: false - name: Generate report for all tests @@ -162,7 +163,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} comment-identifier: "${{ env.WORKFLOW_SHORT_NAME }}-reportgenerator" - comment-content: ${{ steps.reportgenerator.outputs.markdown }} + comment-content: ${{ steps.reportgenerator.outputs.markdown }}${{ steps.test-reporter.outputs.conclusion }} - name: Code Coverage Report uses: irongut/CodeCoverageSummary@v1.3.0 From 6d82795b9b5af03e6e4ba1a686adbb5d38846e1a Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Mon, 24 Jun 2024 01:08:22 +0200 Subject: [PATCH 11/12] TEST-0003 Fix reporter type --- .github/workflows/backend-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index ae9cfdd7..319b0fd1 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -139,12 +139,12 @@ jobs: run: dotnet test ../../tests/backend/WebApi.Tests --no-restore --collect:"XPlat Code Coverage" --logger "trx;LogFileName=test-results.trx" /p:CollectCoverage=true /p:CoverletOutput="../../tests/backend/WebApi.Tests/TestResults/" /p:CoverletOutputFormat=cobertura continue-on-error: true - - uses: hwinther/test-reporter@dotnet-nunit # dorny/test-reporter@v1 + - uses: hwinther/test-reporter@dotnet-nunit # TODO: set back to original once nunit is supported: dorny/test-reporter@v1 id: test-reporter with: name: "WebApi Tests" path: "tests/backend/WebApi.Tests/TestResults/test-results.trx" - reporter: dotnet-trx # TODO: nunit is not supported + reporter: dotnet-nunit fail-on-error: false - name: Generate report for all tests From ea5a4ae5365564f3c771f38ce09751b7203dc82e Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Mon, 24 Jun 2024 01:11:47 +0200 Subject: [PATCH 12/12] TEST-0003 Fix test issue --- .github/workflows/backend-ci.yml | 5 +++-- .../WebApi.Tests/Controllers/BloggingControllerTests.cs | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 319b0fd1..dcbf6097 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -139,12 +139,13 @@ jobs: run: dotnet test ../../tests/backend/WebApi.Tests --no-restore --collect:"XPlat Code Coverage" --logger "trx;LogFileName=test-results.trx" /p:CollectCoverage=true /p:CoverletOutput="../../tests/backend/WebApi.Tests/TestResults/" /p:CoverletOutputFormat=cobertura continue-on-error: true - - uses: hwinther/test-reporter@dotnet-nunit # TODO: set back to original once nunit is supported: dorny/test-reporter@v1 + #- uses: hwinther/test-reporter@dotnet-nunit # TODO: set back to original once nunit is supported: dorny/test-reporter@v1 + - uses: dorny/test-reporter@v1 id: test-reporter with: name: "WebApi Tests" path: "tests/backend/WebApi.Tests/TestResults/test-results.trx" - reporter: dotnet-nunit + reporter: dotnet-trx # dotnet-nunit fail-on-error: false - name: Generate report for all tests diff --git a/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs b/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs index 765a2fc9..1e366709 100644 --- a/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs +++ b/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs @@ -73,9 +73,7 @@ public async Task GetBlog_ValidId_ReturnsBlog() // Assert Assert.That(result, Is.InstanceOf>()); - var okResult = result.Result as OkObjectResult; - Assert.That(okResult, Is.Not.Null); - var returnedBlog = okResult.Value as BlogDto; + var returnedBlog = result.Value; Assert.That(returnedBlog, Is.Not.Null); Assert.Multiple(() => {