Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,26 @@ public async Task<string> ListUpload(
return await sr.ReadToEndAsync();
}

public async Task<string> OptionalUpload([GraphQLType(typeof(UploadType))] Optional<IFile> file)
public async Task<string?> NullableUpload(IFile? file)
{
await using var stream = file.Value!.OpenReadStream();
if (file is null)
{
return null;
}

await using var stream = file.OpenReadStream();
using var sr = new StreamReader(stream, Encoding.UTF8);
return await sr.ReadToEndAsync();
}

public async Task<string?> OptionalUpload([GraphQLType(typeof(UploadType))] Optional<IFile> file)
{
if (!file.HasValue || file.Value is null)
{
return null;
}

await using var stream = file.Value.OpenReadStream();
using var sr = new StreamReader(stream, Encoding.UTF8);
return await sr.ReadToEndAsync();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@

namespace HotChocolate.AspNetCore;

public class HttpMultipartMiddlewareTests : ServerTestBase
public class HttpMultipartMiddlewareTests(TestServerFactory serverFactory) : ServerTestBase(serverFactory)
{
public HttpMultipartMiddlewareTests(TestServerFactory serverFactory)
: base(serverFactory)
{
}

[Fact]
public async Task EmptyForm_Test()
{
Expand Down Expand Up @@ -300,6 +295,115 @@ public async Task Upload_Optional_File()
result.MatchSnapshot();
}

[Fact]
public async Task Upload_Optional_File_Not_Provided()
{
// arrange
var server = CreateStarWarsServer();

// act
var result = await server.PostAsync(
new ClientQueryRequest
{
Query = @"
query ($upload: Upload) {
optionalUpload(file: $upload)
}",
Variables = new Dictionary<string, object?>
{
{ "upload", null }
}
},
"/upload");

// assert
result.MatchInlineSnapshot(
"""
{
"ContentType": "application/graphql-response+json; charset=utf-8",
"StatusCode": "OK",
"Data": {
"optionalUpload": null
},
"Errors": null,
"Extensions": null
}
""");
}

[Fact]
public async Task Upload_Nullable_File()
{
// arrange
var server = CreateStarWarsServer();

const string query = @"
query ($upload: Upload) {
nullableUpload(file: $upload)
}";

var request = JsonConvert.SerializeObject(
new ClientQueryRequest
{
Query = query,
Variables = new Dictionary<string, object?>
{
{ "upload", null }
}
});

// act
var form = new MultipartFormDataContent
{
{ new StringContent(request), "operations" },
{ new StringContent("{ \"1\": [\"variables.upload\"] }"), "map" },
{ new StringContent("abc"), "1", "foo.bar" }
};

form.Headers.Add(HttpHeaderKeys.Preflight, "1");

var result = await server.PostMultipartAsync(form, path: "/upload");

// assert
result.MatchSnapshot();
}

[Fact]
public async Task Upload_Nullable_File_Not_Provided()
{
// arrange
var server = CreateStarWarsServer();

// act
var result = await server.PostAsync(
new ClientQueryRequest
{
Query = @"
query ($upload: Upload) {
nullableUpload(file: $upload)
}",
Variables = new Dictionary<string, object?>
{
{ "upload", null }
}
},
"/upload");

// assert
result.MatchInlineSnapshot(
"""
{
"ContentType": "application/graphql-response+json; charset=utf-8",
"StatusCode": "OK",
"Data": {
"nullableUpload": null
},
"Errors": null,
"Extensions": null
}
""");
}

[Fact]
public async Task Upload_Optional_File_In_InputObject()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ContentType": "application/graphql-response+json; charset=utf-8",
"StatusCode": "OK",
"Data": {
"nullableUpload": "abc"
},
"Errors": null,
"Extensions": null
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,11 @@ private static IOperationRequest BuildOperationRequest(
{
IFeatureCollection? features = null;

if (request.RequiresFileUpload)
if (request.RequiresFileUpload
&& context.RequestContext.Features.Get<IFileLookup>() is { } fileLookup)
{
features = new FeatureCollection();
features.Set(context.RequestContext.Features.GetRequired<IFileLookup>());
features.Set(fileLookup);
}

if (request.Variables.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ private GraphQLHttpRequest CreateHttpBatchRequest(
bool requiresFileUpload,
ref ChunkedArrayWriter? buffer)
{
if (requiresFileUpload)
if (requiresFileUpload
&& context.RequestContext.Features.Get<IFileLookup>() is { } fileLookup)
{
var capacity = originalRequests.Length;

Expand All @@ -361,7 +362,6 @@ private GraphQLHttpRequest CreateHttpBatchRequest(

var batchRequests = ImmutableArray.CreateBuilder<IOperationRequest>(capacity);
var fileEntries = ImmutableArray.CreateBuilder<FileEntry>();
var fileLookup = context.RequestContext.Features.GetRequired<IFileLookup>();
buffer ??= new ChunkedArrayWriter();
var i = 0;

Expand Down Expand Up @@ -486,10 +486,10 @@ private static OperationRequest CreateSingleRequest(
? VariableValues.Empty
: originalRequest.Variables[0];

if (originalRequest.RequiresFileUpload)
if (originalRequest.RequiresFileUpload
&& context.RequestContext.Features.Get<IFileLookup>() is { } fileLookup)
{
writer ??= new ChunkedArrayWriter();
var fileLookup = context.RequestContext.Features.GetRequired<IFileLookup>();
var (cleanedJson, fileMap) = FileEntryBuilder.Build(writer, variables.Values, fileLookup);

return new OperationRequest(
Expand Down Expand Up @@ -535,10 +535,10 @@ private static OperationBatchRequest CreateOperationBatchRequest(
SourceSchemaClientRequest originalRequest,
ref ChunkedArrayWriter? writer)
{
if (originalRequest.RequiresFileUpload)
if (originalRequest.RequiresFileUpload
&& context.RequestContext.Features.Get<IFileLookup>() is { } fileLookup)
{
writer ??= new ChunkedArrayWriter();
var fileLookup = context.RequestContext.Features.GetRequired<IFileLookup>();
var fileEntries = ImmutableArray.CreateBuilder<FileEntry>();
var requests = new OperationRequest[originalRequest.Variables.Length];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ private readonly bool TryParseScalar(
{
if (element.ValueKind is JsonValueKind.String
&& element.GetString() is { Length: > 0 } fileKey
&& _context.Features.GetRequired<IFileLookup>().TryGetFile(fileKey, out _))
&& _context.Features.Get<IFileLookup>() is { } fileLookup
&& fileLookup.TryGetFile(fileKey, out _))
{
value = new StringValueNode($"$.file({fileKey})");
error = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,97 @@ public async Task Upload_List_Of_Files_In_Input_Object_Inline()
await MatchSnapshotAsync(gateway, operation, result, rawRequest: rawRequest);
}

[Fact]
public async Task Upload_Nullable_File_Not_Provided()
{
// arrange
using var server1 = CreateSourceSchema(
"A",
b => b.AddQueryType<SourceSchema1.Query>().AddUploadType());

using var gateway = await CreateCompositeSchemaAsync(
[
("A", server1)
]);

// act
using var client = GraphQLHttpClient.Create(gateway.CreateClient());

var operation = new OperationRequest(
"""
query ($file: Upload) {
nullableUpload(file: $file) {
fileName
contentType
content
}
}
""",
variables: new Dictionary<string, object?>
{
["file"] = null
} );

var request = new GraphQLHttpRequest(operation, new Uri("http://localhost:5000/graphql"))
{
Method = GraphQLHttpMethod.Post
};

// act
var result = await client.SendAsync(request);

// assert
await MatchSnapshotAsync(gateway, operation, result);
}

[Fact]
public async Task Upload_Nullable_File()
{
// arrange
using var server1 = CreateSourceSchema(
"A",
b => b.AddQueryType<SourceSchema1.Query>().AddUploadType());

using var gateway = await CreateCompositeSchemaAsync(
[
("A", server1)
]);

// act
using var client = GraphQLHttpClient.Create(gateway.CreateClient());

var stream = new MemoryStream("abc"u8.ToArray());

var operation = new OperationRequest(
"""
query ($file: Upload) {
nullableUpload(file: $file) {
fileName
contentType
content
}
}
""",
variables: new Dictionary<string, object?>
{
["file"] = new FileReference(() => stream, "test.txt", "text/plain")
});

RawRequest? rawRequest = null;
var request = new GraphQLHttpRequest(operation, new Uri("http://localhost:5000/graphql"))
{
Method = GraphQLHttpMethod.Post,
EnableFileUploads = true,
OnMessageCreated = (_, request, _) => rawRequest = GetRawRequest(request)
};

// act
var result = await client.SendAsync(request);

// assert
await MatchSnapshotAsync(gateway, operation, result, rawRequest: rawRequest);
}

private static RawRequest GetRawRequest(HttpRequestMessage requestMessage)
{
if (requestMessage.Content is not { } content)
Expand Down Expand Up @@ -349,6 +440,16 @@ public class Query
{
public async Task<FileUploadResult> SingleUpload(IFile file) => await ReadFileAsync(file);

public async Task<FileUploadResult?> NullableUpload(IFile? file)
{
if (file is null)
{
return null;
}

return await ReadFileAsync(file);
}

public async Task<FileUploadResult> SingleUploadWithInput(FileInput input)
=> await ReadFileAsync(input.File);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ sourceSchemas:

type Query {
singleUpload(file: Upload!): FileUploadResult!
nullableUpload(file: Upload): FileUploadResult
singleUploadWithInput(input: FileInput!): FileUploadResult!
multiUpload(files: [Upload!]!): [FileUploadResult!]!
multiUploadWithInput(input: FilesInput!): [FileUploadResult!]!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ sourceSchemas:

type Query {
singleUpload(file: Upload!): FileUploadResult!
nullableUpload(file: Upload): FileUploadResult
singleUploadWithInput(input: FileInput!): FileUploadResult!
multiUpload(files: [Upload!]!): [FileUploadResult!]!
multiUploadWithInput(input: FilesInput!): [FileUploadResult!]!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ sourceSchemas:

type Query {
singleUpload(file: Upload!): FileUploadResult!
nullableUpload(file: Upload): FileUploadResult
singleUploadWithInput(input: FileInput!): FileUploadResult!
multiUpload(files: [Upload!]!): [FileUploadResult!]!
multiUploadWithInput(input: FilesInput!): [FileUploadResult!]!
Expand Down
Loading
Loading