Skip to content

Commit 2fb60d2

Browse files
authored
File handling example/ tests (#53)
1 parent 24e3333 commit 2fb60d2

File tree

15 files changed

+1320
-21
lines changed

15 files changed

+1320
-21
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using IeuanWalker.MinimalApi.Endpoints;
2+
3+
namespace ExampleApi.Endpoints.FileHandling.PostListOfFiles;
4+
5+
public class PostFileHandlingListOfFilesEndpoint : IEndpoint<IFormFileCollection, IEnumerable<ResponseModel>>
6+
{
7+
public static void Configure(RouteHandlerBuilder builder)
8+
{
9+
builder
10+
.Post("api/v{version:apiVersion}/FileHandling/ListOfFiles")
11+
.RequestFromForm()
12+
.DisableAntiforgery();
13+
}
14+
15+
public Task<IEnumerable<ResponseModel>> Handle(IFormFileCollection request, CancellationToken ct)
16+
{
17+
IEnumerable<ResponseModel> response = request.Select(x => new ResponseModel
18+
{
19+
FileName = x.FileName,
20+
PropertyName = x.Name,
21+
Size = (int)x.Length
22+
});
23+
24+
return Task.FromResult(response);
25+
}
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace ExampleApi.Endpoints.FileHandling.PostListOfFiles;
2+
3+
public sealed class ResponseModel
4+
{
5+
public required string FileName { get; set; }
6+
public required string PropertyName { get; set; }
7+
public int Size { get; set; }
8+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using IeuanWalker.MinimalApi.Endpoints;
2+
3+
namespace ExampleApi.Endpoints.FileHandling.PostMultipart;
4+
5+
public class PostFileHandlingMultipartEndpoint : IEndpoint<RequestModel, ResponseModel>
6+
{
7+
readonly IHttpContextAccessor _context;
8+
public PostFileHandlingMultipartEndpoint(IHttpContextAccessor context)
9+
{
10+
_context = context ?? throw new ArgumentNullException(nameof(context));
11+
}
12+
public static void Configure(RouteHandlerBuilder builder)
13+
{
14+
builder
15+
.Post("api/v{version:apiVersion}/FileHandling/Multipart")
16+
.RequestFromForm()
17+
.DisableAntiforgery();
18+
}
19+
20+
public Task<ResponseModel> Handle(RequestModel request, CancellationToken ct)
21+
{
22+
ResponseModel response = new()
23+
{
24+
SomeData = request.SomeData,
25+
TotalFileCount = _context.HttpContext!.Request.Form.Files.Count,
26+
SingleFile = MapFile(request.SingleFile),
27+
ReadOnlyList1 = request.ReadOnlyList1.Select(MapFile).ToList(),
28+
ReadOnlyList2 = request.ReadOnlyList2?.Select(MapFile).ToList() ?? [],
29+
FileCollectionList = request.FileCollectionList.Select(MapFile).ToList(),
30+
};
31+
32+
return Task.FromResult(response);
33+
}
34+
35+
static FileInfo MapFile(IFormFile formFile)
36+
{
37+
return new FileInfo
38+
{
39+
FileName = formFile.FileName,
40+
PropertyName = formFile.Name,
41+
Size = (int)formFile.Length,
42+
};
43+
}
44+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace ExampleApi.Endpoints.FileHandling.PostMultipart;
2+
3+
public class RequestModel
4+
{
5+
public required string SomeData { get; set; }
6+
public IFormFile SingleFile { get; set; } = null!;
7+
public required IReadOnlyList<IFormFile> ReadOnlyList1 { get; set; }
8+
public required IReadOnlyList<IFormFile> ReadOnlyList2 { get; set; }
9+
/// <summary>
10+
/// Important: IFormFileCollection doesnt respect property names and binds all files in the request
11+
/// IFormFile and IReadOnlyList respect property names and only bind the files relevant to their property names
12+
/// https://github.com/dotnet/aspnetcore/issues/54999
13+
/// </summary>
14+
public required IFormFileCollection FileCollectionList { get; set; }
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace ExampleApi.Endpoints.FileHandling.PostMultipart;
2+
3+
public sealed class ResponseModel
4+
{
5+
public required string SomeData { get; set; }
6+
public required FileInfo SingleFile { get; set; }
7+
public required int TotalFileCount { get; set; }
8+
public required List<FileInfo> ReadOnlyList1 { get; set; }
9+
public required List<FileInfo> ReadOnlyList2 { get; set; }
10+
public required List<FileInfo> FileCollectionList { get; set; }
11+
}
12+
13+
public sealed class FileInfo
14+
{
15+
public required string FileName { get; set; }
16+
public required string PropertyName { get; set; }
17+
public int Size { get; set; }
18+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using IeuanWalker.MinimalApi.Endpoints;
2+
3+
namespace ExampleApi.Endpoints.FileHandling.PostSingleFile;
4+
5+
public class PostFileHandlingSingleFileEndpoint : IEndpoint<IFormFile, ResponseModel>
6+
{
7+
public static void Configure(RouteHandlerBuilder builder)
8+
{
9+
builder
10+
.Post("api/v{version:apiVersion}/FileHandling/SingleFile")
11+
.RequestFromForm()
12+
.DisableAntiforgery();
13+
}
14+
15+
public Task<ResponseModel> Handle(IFormFile request, CancellationToken ct)
16+
{
17+
ResponseModel response = new()
18+
{
19+
FileName = request.FileName,
20+
PropertyName = request.Name,
21+
Size = (int)request.Length,
22+
};
23+
24+
return Task.FromResult(response);
25+
}
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace ExampleApi.Endpoints.FileHandling.PostSingleFile;
2+
3+
public sealed class ResponseModel
4+
{
5+
public required string FileName { get; set; }
6+
public required string PropertyName { get; set; }
7+
public int Size { get; set; }
8+
}

example/ExampleApi/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
builder.AddApiVersioning();
99
builder.AddEndpoints();
1010
builder.Services.AddSingleton<ITodoStore, InMemoryTodoStore>();
11+
builder.Services.AddHttpContextAccessor();
1112
builder.AddScalar();
1213

1314
WebApplication app = builder.Build();
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using ListOfFiles = ExampleApi.Endpoints.FileHandling.PostListOfFiles;
4+
5+
namespace ExampleApi.IntegrationTests.Endpoints.FileHandling;
6+
7+
/// <summary>
8+
/// Integration tests for PostFileHandlingListOfFilesEndpoint
9+
/// </summary>
10+
public class PostListOfFilesTests : IClassFixture<ExampleApiWebApplicationFactory>
11+
{
12+
readonly HttpClient _client;
13+
14+
public PostListOfFilesTests(ExampleApiWebApplicationFactory factory)
15+
{
16+
_client = factory.CreateClient();
17+
}
18+
19+
[Fact]
20+
public async Task PostListOfFiles_WithMultipleFiles_ReturnsAllFileDetails()
21+
{
22+
// Arrange
23+
using MultipartFormDataContent content = new();
24+
25+
using MemoryStream file1Stream = new("First file content"u8.ToArray());
26+
using StreamContent file1Content = new(file1Stream);
27+
file1Content.Headers.ContentType = new("application/pdf");
28+
content.Add(file1Content, "files", "document1.pdf");
29+
30+
using MemoryStream file2Stream = new("Second file content"u8.ToArray());
31+
using StreamContent file2Content = new(file2Stream);
32+
file2Content.Headers.ContentType = new("text/plain");
33+
content.Add(file2Content, "files", "document2.txt");
34+
35+
using MemoryStream file3Stream = new("Third file content"u8.ToArray());
36+
using StreamContent file3Content = new(file3Stream);
37+
file3Content.Headers.ContentType = new("image/png");
38+
content.Add(file3Content, "files", "image.png");
39+
40+
// Act
41+
HttpResponseMessage response = await _client.PostAsync("/api/v1/FileHandling/ListOfFiles", content);
42+
43+
// Assert
44+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
45+
ListOfFiles.ResponseModel[]? result = await response.Content.ReadFromJsonAsync<ListOfFiles.ResponseModel[]>();
46+
result.ShouldNotBeNull();
47+
result.Length.ShouldBe(3);
48+
49+
result[0].FileName.ShouldBe("document1.pdf");
50+
result[0].Size.ShouldBe(18); // "First file content"
51+
52+
result[1].FileName.ShouldBe("document2.txt");
53+
result[1].Size.ShouldBe(19); // "Second file content"
54+
55+
result[2].FileName.ShouldBe("image.png");
56+
result[2].Size.ShouldBe(18); // "Third file content"
57+
}
58+
59+
[Fact]
60+
public async Task PostListOfFiles_WithSingleFile_ReturnsSingleFileDetails()
61+
{
62+
// Arrange
63+
using MultipartFormDataContent content = new();
64+
using MemoryStream fileStream = new("Single file"u8.ToArray());
65+
using StreamContent fileContent = new(fileStream);
66+
fileContent.Headers.ContentType = new("text/plain");
67+
content.Add(fileContent, "file", "single.txt");
68+
69+
// Act
70+
HttpResponseMessage response = await _client.PostAsync("/api/v1/FileHandling/ListOfFiles", content);
71+
72+
// Assert
73+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
74+
ListOfFiles.ResponseModel[]? result = await response.Content.ReadFromJsonAsync<ListOfFiles.ResponseModel[]>();
75+
result.ShouldNotBeNull();
76+
result.Length.ShouldBe(1);
77+
result[0].FileName.ShouldBe("single.txt");
78+
result[0].Size.ShouldBe(11); // "Single file"
79+
}
80+
81+
[Fact]
82+
public async Task PostListOfFiles_WithEmptyCollection_ReturnsEmptyArray()
83+
{
84+
// Arrange
85+
using MultipartFormDataContent content = new();
86+
// Add a dummy field to make it a valid multipart request
87+
content.Add(new StringContent(""), "dummy");
88+
89+
// Act
90+
HttpResponseMessage response = await _client.PostAsync("/api/v1/FileHandling/ListOfFiles", content);
91+
92+
// Assert
93+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
94+
ListOfFiles.ResponseModel[]? result = await response.Content.ReadFromJsonAsync<ListOfFiles.ResponseModel[]>();
95+
result.ShouldNotBeNull();
96+
result.ShouldBeEmpty();
97+
}
98+
99+
[Fact]
100+
public async Task PostListOfFiles_WithDifferentPropertyNames_PreservesPropertyNames()
101+
{
102+
// Arrange
103+
using MultipartFormDataContent content = new();
104+
105+
using MemoryStream file1Stream = new("File 1"u8.ToArray());
106+
using StreamContent file1Content = new(file1Stream);
107+
content.Add(file1Content, "property1", "file1.txt");
108+
109+
using MemoryStream file2Stream = new("File 2"u8.ToArray());
110+
using StreamContent file2Content = new(file2Stream);
111+
content.Add(file2Content, "property2", "file2.txt");
112+
113+
// Act
114+
HttpResponseMessage response = await _client.PostAsync("/api/v1/FileHandling/ListOfFiles", content);
115+
116+
// Assert
117+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
118+
ListOfFiles.ResponseModel[]? result = await response.Content.ReadFromJsonAsync<ListOfFiles.ResponseModel[]>();
119+
result.ShouldNotBeNull();
120+
result.Length.ShouldBe(2);
121+
result[0].PropertyName.ShouldBe("property1");
122+
result[1].PropertyName.ShouldBe("property2");
123+
}
124+
125+
[Fact]
126+
public async Task PostListOfFiles_WithEmptyFiles_ReturnsZeroSizes()
127+
{
128+
// Arrange
129+
using MultipartFormDataContent content = new();
130+
131+
using MemoryStream file1Stream = new([]);
132+
using StreamContent file1Content = new(file1Stream);
133+
content.Add(file1Content, "files", "empty1.txt");
134+
135+
using MemoryStream file2Stream = new([]);
136+
using StreamContent file2Content = new(file2Stream);
137+
content.Add(file2Content, "files", "empty2.txt");
138+
139+
// Act
140+
HttpResponseMessage response = await _client.PostAsync("/api/v1/FileHandling/ListOfFiles", content);
141+
142+
// Assert
143+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
144+
ListOfFiles.ResponseModel[]? result = await response.Content.ReadFromJsonAsync<ListOfFiles.ResponseModel[]>();
145+
result.ShouldNotBeNull();
146+
result.Length.ShouldBe(2);
147+
result[0].Size.ShouldBe(0);
148+
result[1].Size.ShouldBe(0);
149+
}
150+
}

0 commit comments

Comments
 (0)