Skip to content

Commit

Permalink
Make endpoint to read images as stream from relative repo path (#11317)
Browse files Browse the repository at this point in the history
* Make endpoint to read images as bytearray from relative repo path

* Allow imagefilepath to be arbitrary long

* Add tests

* Use stream instead of read all bytes

* Add tests for non-existing iamges
  • Loading branch information
standeren authored Oct 12, 2023
1 parent e882a6b commit 5b802a2
Show file tree
Hide file tree
Showing 9 changed files with 471 additions and 1 deletion.
36 changes: 35 additions & 1 deletion backend/src/Designer/Controllers/PreviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ namespace Altinn.Studio.Designer.Controllers
/// </summary>
[Authorize]
[AutoValidateAntiforgeryToken]
[Route("{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}")]
// Uses regex to not match on designer since the call from frontend to get the iframe for app-frontend,
// `designer/html/preview.html`, will match on Image-endpoint which is a fetch-all route
[Route("{org:regex(^(?!designer))}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}")]
public class PreviewController : Controller
{
private readonly IHttpContextAccessor _httpContextAccessor;
Expand Down Expand Up @@ -77,6 +79,38 @@ public ActionResult<string> PreviewStatus()
return Ok();
}

/// <summary>
/// Action for getting local app-images
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <param name="imageFilePath">A path to the image location, including file name, consisting of an arbitrary amount of directories</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The specified local app-image as Stream</returns>
[HttpGet]
[Route("{*imageFilePath}")]
public FileStreamResult Image(string org, string app, string imageFilePath, CancellationToken cancellationToken)
{

if (imageFilePath.Contains('/'))
{
string imageFileName = string.Empty;
string[] segments = imageFilePath.Split('/');

foreach (string segment in segments)
{
imageFileName = Path.Combine(imageFileName, segment);
}

imageFilePath = imageFileName;
}

string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext);
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
Stream imageStream = altinnAppGitRepository.GetImage(imageFilePath);
return new FileStreamResult(imageStream, MimeTypeMap.GetMimeType(Path.GetExtension(imageFilePath).ToLower()));
}

/// <summary>
/// Action for getting the application metadata
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions backend/src/Designer/Filters/IO/IoExceptionFilterAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ public override void OnException(ExceptionContext context)
// TODO: Implement custom IO exceptions Error Codes
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, IoErrorCodes.ResourceNotFound, HttpStatusCode.NotFound)) { StatusCode = (int)HttpStatusCode.NotFound };
}
if (context.Exception is DirectoryNotFoundException)
{
// TODO: Implement custom IO exceptions Error Codes
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, IoErrorCodes.ResourceNotFound, HttpStatusCode.NotFound)) { StatusCode = (int)HttpStatusCode.NotFound };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class AltinnAppGitRepository : AltinnGitRepository
private const string CONFIG_FOLDER_PATH = "App/config/";
private const string OPTIONS_FOLDER_PATH = "App/options/";
private const string LAYOUTS_FOLDER_NAME = "App/ui/";
private const string IMAGES_FOLDER_NAME = "App/wwwroot/";
private const string LAYOUTS_IN_SET_FOLDER_NAME = "layouts/";
private const string LANGUAGE_RESOURCE_FOLDER_NAME = "texts/";
private const string MARKDOWN_TEXTS_FOLDER_NAME = "md/";
Expand Down Expand Up @@ -759,6 +760,19 @@ public Stream GetProcessDefinitionFile()
return OpenStreamByRelativePath(ProcessDefinitionFilePath);
}

/// <summary>
/// Gets specified image from App/wwwroot folder of local repo
/// </summary>
/// <param name="imageFilePath">The file path of the image</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The image as stream</returns>
public Stream GetImage(string imageFilePath, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string imagePath = GetPathToImage(imageFilePath);
return OpenStreamByRelativePath(imagePath);
}

/// <summary>
/// Gets the relative path to a model.
/// </summary>
Expand All @@ -774,6 +788,11 @@ private static string GetPathToTexts()
return Path.Combine(CONFIG_FOLDER_PATH, LANGUAGE_RESOURCE_FOLDER_NAME);
}

private static string GetPathToImage(string imageFilePath)
{
return Path.Combine(IMAGES_FOLDER_NAME, imageFilePath);
}

private static string GetPathToJsonTextsFile(string fileName)
{
return fileName.IsNullOrEmpty() ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ protected HttpClient HttpClient
/// </summary>
protected abstract void ConfigureTestServices(IServiceCollection services);

protected Action<IServiceCollection> ConfigureTestForSpecificTest { get; set; } = delegate { };

/// <summary>
/// Location of the assembly of the executing unit test.
/// </summary>
Expand Down Expand Up @@ -69,6 +71,7 @@ protected virtual HttpClient GetTestClient()
{
builder.ConfigureAppConfiguration((_, conf) => { conf.AddJsonFile(configPath); });
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureServices(ConfigureTestForSpecificTest);
}).CreateDefaultClient(new ApiTestsAuthAndCookieDelegatingHandler(), new CookieContainerHandler());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Services.Interfaces;
using Designer.Tests.Utils;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;

namespace Designer.Tests.Controllers.PreviewController
{
public class GetImageTests : PreviewControllerTestsBase<GetImageTests>, IClassFixture<WebApplicationFactory<Program>>
{

public GetImageTests(WebApplicationFactory<Program> factory) : base(factory)
{
}

[Fact]
public async Task Get_Image_From_Wwww_Root_Folder_Ok()
{
byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, App, Developer, "App/wwwroot/AltinnD-logo.svg");

string dataPathWithData = $"{Org}/{App}/AltinnD-logo.svg";

using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

var result = await response.Content.ReadAsByteArrayAsync();
Assert.NotNull(result);
Assert.Equal(expectedImageData, result);
}

[Fact]
public async Task Get_Image_From_Sub_Folder_Ok()
{
byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, App, Developer, "App/wwwroot/images/AltinnD-logo.svg");

string dataPathWithData = $"{Org}/{App}/images/AltinnD-logo.svg";

using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

var result = await response.Content.ReadAsByteArrayAsync();
Assert.NotNull(result);
Assert.Equal(expectedImageData, result);
}

[Fact]
public async Task Get_Image_From_Sub_Sub_Folder_Ok()
{
byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, App, Developer, "App/wwwroot/images/subImagesFolder/AltinnD-logo.svg");

string dataPathWithData = $"{Org}/{App}/images/subImagesFolder/AltinnD-logo.svg";

using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

byte[] result = await response.Content.ReadAsByteArrayAsync();
Assert.NotNull(result);
Assert.Equal(expectedImageData, result);
}

[Fact]
public async Task Get_Image_Non_Existing_Folder_Returns_NotFound()
{
string dataPathWithData = $"{Org}/{App}/images/subImagesFolder/SubSubImageFolder/AltinnD-logo.svg";

using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task Get_Image_Non_Existing_Image_Return_NotFound()
{
string dataPathWithData = $"{Org}/{App}/images/subImagesFolder/non-existing-image.svg";

using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task Call_To_Get_Designer_Iframe_Does_Not_Hit_Image_EndPoint()
{
Mock<IAltinnGitRepositoryFactory> factMock = new();
ConfigureTestForSpecificTest = s =>
{
s.AddTransient(_ => factMock.Object);
};

string dataPathWithData = "designer/html/path/some-file.jpg";
using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
factMock.Verify(x => x.GetAltinnAppGitRepository(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
}
}
11 changes: 11 additions & 0 deletions backend/tests/Designer.Tests/Utils/TestDataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,17 @@ public static string GetFileFromRepo(string org, string repository, string devel
return string.Empty;
}

public static byte[] GetFileAsByteArrayFromRepo(string org, string repository, string developer, string relativePath)
{
string filePath = Path.Combine(GetTestDataRepositoryDirectory(org, repository, developer), relativePath);
if (File.Exists(filePath))
{
return File.ReadAllBytes(filePath);
}

return new byte[0];
}

public static bool FileExistsInRepo(string org, string repository, string developer, string relativePath)
{
string filePath = Path.Combine(GetTestDataRepositoryDirectory(org, repository, developer), relativePath);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 5b802a2

Please sign in to comment.