diff --git a/Doppler.HtmlEditorApi.Test/Repositories.DopplerDb/DopplerTemplateRepositoryTest.cs b/Doppler.HtmlEditorApi.Test/Repositories.DopplerDb/DopplerTemplateRepositoryTest.cs index d7de2855..b1e4cbf5 100644 --- a/Doppler.HtmlEditorApi.Test/Repositories.DopplerDb/DopplerTemplateRepositoryTest.cs +++ b/Doppler.HtmlEditorApi.Test/Repositories.DopplerDb/DopplerTemplateRepositoryTest.cs @@ -135,4 +135,166 @@ public async Task UpdateTemplate_should_execute_the_right_query_and_parameters() dbQuery.VerifySqlQueryContains("PreviewImage = @PreviewImage"); dbQuery.VerifySqlQueryContains("Name = @Name"); } + + [Fact] + public async Task CreatePrivateTemplate_should_execute_the_right_query_and_parameters() + { + // Arrange + var dbContextMock = new Mock(); + dbContextMock.Setup(x => + x.ExecuteAsync(It.IsAny>())) + .ReturnsAsync(new CreatePrivateTemplateDbQuery.Result() { NewTemplateId = 123 }); + + var sut = new DopplerTemplateRepository(dbContextMock.Object); + + var previewImage = "NEW PREVIEW IMAGE"; + var name = "NEW NAME"; + var htmlComplete = "NEW HTML CONTENT"; + var meta = "{\"test\":\"NEW META\"}"; + var templateModel = new TemplateModel( + TemplateId: 0, + IsPublic: false, + PreviewImage: previewImage, + Name: name, + Content: new UnlayerTemplateContentData( + HtmlComplete: htmlComplete, + Meta: meta)); + var accountName = "test@test"; + + // Act + var result = await sut.CreatePrivateTemplate(accountName, templateModel); + + // Assert + var dbQuery = dbContextMock.VerifyAndGetSingleItemDbQuery(); + dbQuery.VerifySqlParametersContain("EditorType", _unlayerEditorType); + dbQuery.VerifySqlParametersContain("HtmlCode", htmlComplete); + dbQuery.VerifySqlParametersContain("Meta", meta); + dbQuery.VerifySqlParametersContain("PreviewImage", previewImage); + dbQuery.VerifySqlParametersContain("Name", name); + dbQuery.VerifySqlQueryContains("FROM [User] u"); + dbQuery.VerifySqlQueryContains("WHERE u.Email = @AccountName"); + dbQuery.VerifySqlQueryContains("INSERT INTO Template (IdUser, EditorType, HtmlCode, Meta, PreviewImage, Name, Active)"); + dbQuery.VerifySqlQueryContains("u.IdUser AS IdUser"); + dbQuery.VerifySqlQueryContains("@EditorType AS EditorType"); + dbQuery.VerifySqlQueryContains("@HtmlCode AS HtmlCode"); + dbQuery.VerifySqlQueryContains("@Meta AS Meta"); + dbQuery.VerifySqlQueryContains("@PreviewImage AS PreviewImage"); + dbQuery.VerifySqlQueryContains("@Name AS Name"); + dbQuery.VerifySqlQueryContains("1 AS Active"); + dbQuery.VerifySqlQueryContains("OUTPUT INSERTED.idTemplate AS NewTemplateId"); + } + + [Fact] + public async Task CreatePrivateTemplate_throw_when_there_are_no_rows_inserted() + { + // Arrange + var dbContextMock = new Mock(); + dbContextMock.Setup(x => + x.ExecuteAsync(It.IsAny>())) + .ReturnsAsync((CreatePrivateTemplateDbQuery.Result)null); + + var sut = new DopplerTemplateRepository(dbContextMock.Object); + + var templateModel = new TemplateModel( + TemplateId: 0, + IsPublic: false, + PreviewImage: "NEW PREVIEW IMAGE", + Name: "NEW NAME", + Content: new UnlayerTemplateContentData( + HtmlComplete: "NEW HTML CONTENT", + Meta: "{\"test\":\"NEW META\"}")); + var accountName = "test@test"; + + // Act + var action = async () => await sut.CreatePrivateTemplate(accountName, templateModel); + + // Assert + var exception = await Assert.ThrowsAsync(action); + Assert.Equal("accountName", exception.ParamName); + Assert.Equal($"Account with name '{accountName}' does not exist. (Parameter 'accountName')", exception.Message); + } + + [Fact] + public async Task CreatePrivateTemplate_should_throw_when_TemplateId_is_not_0() + { + // Arrange + var templateId = 123; + var dbContextMock = new Mock(); + + var sut = new DopplerTemplateRepository(dbContextMock.Object); + + var templateModel = new TemplateModel( + TemplateId: templateId, + IsPublic: false, + PreviewImage: "NEW PREVIEW IMAGE", + Name: "NEW NAME", + Content: new UnlayerTemplateContentData( + HtmlComplete: "NEW HTML CONTENT", + Meta: "{\"test\":\"NEW META\"}")); + var accountName = "test@test"; + + // Act + var action = async () => await sut.CreatePrivateTemplate(accountName, templateModel); + + // Assert + var exception = await Assert.ThrowsAsync(action); + Assert.Equal("templateModel", exception.ParamName); + Assert.Equal("TemplateId should not be set to create a new private template (Parameter 'templateModel')", exception.Message); + dbContextMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task CreatePrivateTemplate_should_throw_when_IsPublic_is_true() + { + // Arrange + var isPublic = true; + var dbContextMock = new Mock(); + + var sut = new DopplerTemplateRepository(dbContextMock.Object); + + var templateModel = new TemplateModel( + TemplateId: 0, + IsPublic: isPublic, + PreviewImage: "NEW PREVIEW IMAGE", + Name: "NEW NAME", + Content: new UnlayerTemplateContentData( + HtmlComplete: "NEW HTML CONTENT", + Meta: "{\"test\":\"NEW META\"}")); + var accountName = "test@test"; + + // Act + var action = async () => await sut.CreatePrivateTemplate(accountName, templateModel); + + // Assert + var exception = await Assert.ThrowsAsync(action); + Assert.Equal("templateModel", exception.ParamName); + Assert.Equal("IsPublic should be false to create a new private template (Parameter 'templateModel')", exception.Message); + dbContextMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task CreatePrivateTemplate_should_throw_when_the_template_is_not_unlayer_one() + { + // Arrange + var content = new UnknownTemplateContentData(_msEditorType); + var dbContextMock = new Mock(); + + var sut = new DopplerTemplateRepository(dbContextMock.Object); + + var templateModel = new TemplateModel( + TemplateId: 0, + IsPublic: false, + PreviewImage: "NEW PREVIEW IMAGE", + Name: "NEW NAME", + Content: content); + var accountName = "test@test"; + + // Act + var action = async () => await sut.CreatePrivateTemplate(accountName, templateModel); + + // Assert + var exception = await Assert.ThrowsAsync(action); + Assert.Equal("Unsupported template content type Doppler.HtmlEditorApi.Domain.UnknownTemplateContentData", exception.Message); + dbContextMock.VerifyNoOtherCalls(); + } } diff --git a/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/GetTemplateTest.cs b/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/GetTemplateTest.cs index 411ad559..b695d0de 100644 --- a/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/GetTemplateTest.cs +++ b/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/GetTemplateTest.cs @@ -195,11 +195,10 @@ public async Task GET_template_should_error_when_template_content_is_mseditor(st var responseContentJson = responseContentDoc.RootElement; // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.Equal("https://httpstatuses.io/500", responseContentJson.GetProperty("type").GetString()); - Assert.Equal("Internal Server Error", responseContentJson.GetProperty("title").GetString()); + Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode); + Assert.Equal("Not Implemented", responseContentJson.GetProperty("title").GetString()); Assert.Equal("Unsupported template content type Doppler.HtmlEditorApi.Domain.UnknownTemplateContentData", responseContentJson.GetProperty("detail").GetString()); - Assert.Equal(500, responseContentJson.GetProperty("status").GetInt32()); + Assert.Equal(501, responseContentJson.GetProperty("status").GetInt32()); } [Theory] @@ -284,6 +283,8 @@ public async Task GET_template_should_return_not_found_when_template_is_public(s // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.Equal($"It is a public template, use /shared/templates/{idTemplate}", responseContent); + Assert.Contains($"\"detail\":\"It is a public template, use /shared/templates/{idTemplate}\"", responseContent); + Assert.Contains("\"title\":\"Not Found\"", responseContent); + Assert.Contains("\"status\":404", responseContent); } } diff --git a/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/PutTemplateTest.cs b/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/PutTemplateTest.cs index 1928b126..c8244447 100644 --- a/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/PutTemplateTest.cs +++ b/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/PutTemplateTest.cs @@ -232,7 +232,9 @@ public async Task PUT_template_should_return_404_when_template_does_not_exist() // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.Equal("Template not found, belongs to a different account, or it is a public template.", responseContent); + Assert.Contains("Template not found, belongs to a different account, or it is a public template", responseContent); + Assert.Contains("\"title\":\"Not Found\"", responseContent); + Assert.Contains("\"status\":404", responseContent); } [Fact] @@ -273,7 +275,9 @@ public async Task PUT_template_should_return_404_when_template_is_public() // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.Equal("Template not found, belongs to a different account, or it is a public template.", responseContent); + Assert.Contains("\"detail\":\"Template not found, belongs to a different account, or it is a public template.\"", responseContent); + Assert.Contains("\"title\":\"Not Found\"", responseContent); + Assert.Contains("\"status\":404", responseContent); } [Fact] diff --git a/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/from-template/_/PostTemplateFromTemplateTest.cs b/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/from-template/_/PostTemplateFromTemplateTest.cs new file mode 100644 index 00000000..e5431568 --- /dev/null +++ b/Doppler.HtmlEditorApi.Test/http/accounts/_/templates/_/from-template/_/PostTemplateFromTemplateTest.cs @@ -0,0 +1,216 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; +using Doppler.HtmlEditorApi.DataAccess; +using Doppler.HtmlEditorApi.Domain; +using Doppler.HtmlEditorApi.Repositories; +using Doppler.HtmlEditorApi.Repositories.DopplerDb.Queries; +using Doppler.HtmlEditorApi.Test.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Doppler.HtmlEditorApi; + +public class PostTemplateFromTemplateTest : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + private readonly ITestOutputHelper _output; + + public PostTemplateFromTemplateTest(WebApplicationFactory factory, ITestOutputHelper output) + { + _factory = factory; + _output = output; + } + + [Theory] + [InlineData("/accounts/x@x.com/templates/from-template/456", HttpStatusCode.Unauthorized)] + public async Task GET_template_should_require_token(string url, HttpStatusCode expectedStatusCode) + { + // Arrange + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions()); + + // Act + var response = await client.PostAsync(url, null); + _output.WriteLine(response.GetHeadersAsString()); + + // Assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal("Bearer", response.Headers.WwwAuthenticate.ToString()); + } + + [Theory] + [InlineData("/accounts/x@x.com/templates/from-template/456", TestUsersData.TOKEN_TEST1_EXPIRE_20330518, HttpStatusCode.Forbidden)] + [InlineData("/accounts/x@x.com/templates/from-template/456", TestUsersData.TOKEN_EXPIRE_20330518, HttpStatusCode.Forbidden)] + public async Task GET_template_should_not_accept_the_token_of_another_account(string url, string token, HttpStatusCode expectedStatusCode) + { + // Arrange + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions()); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + var response = await client.PostAsync(url, null); + _output.WriteLine(response.GetHeadersAsString()); + + // Assert + Assert.Equal(expectedStatusCode, response.StatusCode); + } + + [Theory] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/456", TestUsersData.TOKEN_TEST1_EXPIRE_20010908, HttpStatusCode.Unauthorized)] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/456", TestUsersData.TOKEN_SUPERUSER_EXPIRE_20010908, HttpStatusCode.Unauthorized)] + public async Task GET_template_should_not_accept_a_expired_token(string url, string token, HttpStatusCode expectedStatusCode) + { + // Arrange + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions()); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + var response = await client.PostAsync(url, null); + _output.WriteLine(response.GetHeadersAsString()); + + // Assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Contains("Bearer", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("invalid_token", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("token expired", response.Headers.WwwAuthenticate.ToString()); + } + + [Theory] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/459", TestUsersData.TOKEN_TEST1_EXPIRE_20330518, TestUsersData.EMAIL_TEST1, 459)] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/459", TestUsersData.TOKEN_SUPERUSER_EXPIRE_20330518, TestUsersData.EMAIL_TEST1, 459)] + [InlineData("/accounts/otro@test.com/templates/from-template/459", TestUsersData.TOKEN_SUPERUSER_EXPIRE_20330518, "otro@test.com", 459)] + public async Task GET_template_should_accept_right_tokens_and_return_404_when_not_exist(string url, string token, string accountName, int baseTemplateId) + { + // Arrange + TemplateModel templateModel = null; + var repositoryMock = new Mock(); + + repositoryMock + .Setup(x => x.GetOwnOrPublicTemplate(accountName, baseTemplateId)) + .ReturnsAsync(templateModel); + + var client = _factory.CreateSutClient( + serviceToOverride1: repositoryMock.Object, + token: token); + + // Act + var response = await client.PostAsync(url, null); + _output.WriteLine(response.GetHeadersAsString()); + + // Assert + repositoryMock.VerifyAll(); + repositoryMock.VerifyNoOtherCalls(); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/459", TestUsersData.TOKEN_TEST1_EXPIRE_20330518, TestUsersData.EMAIL_TEST1, 459)] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/459", TestUsersData.TOKEN_SUPERUSER_EXPIRE_20330518, TestUsersData.EMAIL_TEST1, 459)] + [InlineData("/accounts/otro@test.com/templates/from-template/459", TestUsersData.TOKEN_SUPERUSER_EXPIRE_20330518, "otro@test.com", 459)] + public async Task GET_template_should_accept_right_tokens_and_call_repository_and_return_createdResourceId(string url, string token, string accountName, int baseTemplateId) + { + // Arrange + const int unlayerEditorType = 5; + var newTemplateId = 8; + var expectedSchemaVersion = 999; + var isPublic = true; + var previewImage = "PreviewImage"; + var name = "Name"; + var htmlComplete = ""; + var meta = JsonSerializer.Serialize(new + { + schemaVersion = expectedSchemaVersion + }); + + var dbContextMock = new Mock(); + + dbContextMock.Setup(x => + x.ExecuteAsync(new GetTemplateByIdWithStatusDbQuery(baseTemplateId, accountName))) + .ReturnsAsync(new GetTemplateByIdWithStatusDbQuery.Result() + { + IsPublic = isPublic, + EditorType = unlayerEditorType, + HtmlCode = htmlComplete, + Meta = meta, + PreviewImage = previewImage, + Name = name, + }); + + dbContextMock.Setup(x => + x.ExecuteAsync(new CreatePrivateTemplateDbQuery( + accountName, + unlayerEditorType, + htmlComplete, + meta, + previewImage, + name))) + .ReturnsAsync(new CreatePrivateTemplateDbQuery.Result() + { + NewTemplateId = newTemplateId + }); + + var client = _factory.CreateSutClient( + serviceToOverride1: dbContextMock.Object, + token: token); + + // Act + var response = await client.PostAsync(url, null); + var headers = response.GetHeadersAsString(); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + dbContextMock.VerifyAll(); + dbContextMock.VerifyNoOtherCalls(); + Assert.Matches($$"""{"createdResourceId":{{newTemplateId}}}""", responseContent); + Assert.Contains($"Location: http://localhost/accounts/{accountName}/templates/{newTemplateId}", headers); + } + + [Theory] + [InlineData($"/accounts/{TestUsersData.EMAIL_TEST1}/templates/from-template/459", TestUsersData.TOKEN_TEST1_EXPIRE_20330518, TestUsersData.EMAIL_TEST1, 459)] + public async Task GET_template_should_error_when_base_template_content_is_mseditor(string url, string token, string accountName, int baseTemplateId) + { + // Arrange + var editorType = 4; + var isPublic = true; + var previewImage = "PreviewImage"; + var name = "Name"; + var contentData = new UnknownTemplateContentData(editorType); + + var templateModel = new TemplateModel( + TemplateId: baseTemplateId, + IsPublic: isPublic, + PreviewImage: previewImage, + Name: name, + Content: contentData); + + var repositoryMock = new Mock(); + + repositoryMock + .Setup(x => x.GetOwnOrPublicTemplate(accountName, baseTemplateId)) + .ReturnsAsync(templateModel); + + var client = _factory.CreateSutClient( + serviceToOverride1: repositoryMock.Object, + token: token); + + // Act + var response = await client.PostAsync(url, null); + _output.WriteLine(response.GetHeadersAsString()); + var responseContent = await response.Content.ReadAsStringAsync(); + using var responseContentDoc = JsonDocument.Parse(responseContent); + var responseContentJson = responseContentDoc.RootElement; + + // Assert + repositoryMock.VerifyAll(); + repositoryMock.VerifyNoOtherCalls(); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("https://httpstatuses.io/500", responseContentJson.GetProperty("type").GetString()); + Assert.Equal("Internal Server Error", responseContentJson.GetProperty("title").GetString()); + Assert.Equal("Unsupported template content type Doppler.HtmlEditorApi.Domain.UnknownTemplateContentData", responseContentJson.GetProperty("detail").GetString()); + Assert.Equal(500, responseContentJson.GetProperty("status").GetInt32()); + } +} diff --git a/Doppler.HtmlEditorApi/ApiModels/ResourceCreated.cs b/Doppler.HtmlEditorApi/ApiModels/ResourceCreated.cs new file mode 100644 index 00000000..99f6b62b --- /dev/null +++ b/Doppler.HtmlEditorApi/ApiModels/ResourceCreated.cs @@ -0,0 +1,3 @@ +namespace Doppler.HtmlEditorApi.ApiModels; + +public record ResourceCreated(int createdResourceId); diff --git a/Doppler.HtmlEditorApi/Controllers/TemplatesController.cs b/Doppler.HtmlEditorApi/Controllers/TemplatesController.cs index 995bd5b1..95b3a954 100644 --- a/Doppler.HtmlEditorApi/Controllers/TemplatesController.cs +++ b/Doppler.HtmlEditorApi/Controllers/TemplatesController.cs @@ -5,6 +5,8 @@ using Doppler.HtmlEditorApi.DopplerSecurity; using Doppler.HtmlEditorApi.Repositories; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; namespace Doppler.HtmlEditorApi.Controllers @@ -21,47 +23,58 @@ public TemplatesController(ITemplateRepository templateRepository) } [Authorize(Policies.OwnResourceOrSuperUser)] - [HttpGet("/accounts/{accountName}/templates/{templateId}")] - public async Task> GetTemplate(string accountName, int templateId) + [HttpGet("/accounts/{accountName}/templates/{templateId}", Name = "GetTemplate")] + public async Task, ProblemHttpResult, Ok