-
Notifications
You must be signed in to change notification settings - Fork 57
Add support for send file upload API #488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add support for send file upload API #488
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Adds support for sending (uploading) a created file upload to the Notion API, including client, request/response models, REST client multipart handling, and accompanying unit/integration tests.
- Introduces SendFileUploadRequest/Response models and FileUploadsClient.SendAsync with validation.
- Adds multipart/form-data PostAsync overload to RestClient for streaming file data.
- Extends tests (unit + integration) and project assets (logo file) for end‑to‑end upload flow.
Reviewed Changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
File | Description |
---|---|
Test/Notion.UnitTests/FileUploadClientTests.cs | Adds unit tests for SendAsync argument validation and success path. |
Test/Notion.IntegrationTests/Notion.IntegrationTests.csproj | Includes image asset needed for integration test file upload. |
Test/Notion.IntegrationTests/FIleUploadsClientTests.cs | Adds integration test for full single-part upload flow (and introduces naming/disposal concerns). |
Src/Notion.Client/RestClient/RestClient.cs | Adds multipart/form-data PostAsync overload to send file streams. |
Src/Notion.Client/RestClient/IRestClient.cs | Declares new multipart PostAsync overload (missing XML docs). |
Src/Notion.Client/Api/FileUploads/Send/Response/SendFileUploadResponse.cs | Adds response model inheriting existing FileObjectResponse. |
Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs | Adds request builder implementing form/path parameter interfaces. |
Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadPathParameters.cs | Defines path parameter interface for file upload id. |
Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs | Defines form data interface (includes possibly misleading JsonProperty attribute). |
Src/Notion.Client/Api/FileUploads/Send/Request/FileData.cs | Defines file metadata + stream container. |
Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs | Implements SendAsync with validation (missing file null checks, minor exception choice issue). |
Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs | Adds SendAsync method to public interface with summary. |
Src/Notion.Client/Api/ApiEndpoints.cs | Adds endpoint helper for send action. |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() => _fileUploadClient.SendAsync(null)); | ||
Assert.Equal("sendFileUploadRequest", exception.ParamName); | ||
Assert.Equal("Value cannot be null. (Parameter 'sendFileUploadRequest')", exception.Message); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Asserting the full framework-generated exception.Message is brittle (it can vary by runtime or localization); asserting ParamName plus exception type is sufficient—remove the line 77 exact message assertion.
Assert.Equal("Value cannot be null. (Parameter 'sendFileUploadRequest')", exception.Message); |
Copilot uses AI. Check for mistakes.
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() => _fileUploadClient.SendAsync(request)); | ||
Assert.Equal("FileUploadId", exception.ParamName); | ||
Assert.Equal("Value cannot be null. (Parameter 'FileUploadId')", exception.Message); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Same brittleness: remove or relax the exact exception.Message assertion on line 89; keep validating exception type and ParamName.
Assert.Equal("Value cannot be null. (Parameter 'FileUploadId')", exception.Message); |
Copilot uses AI. Check for mistakes.
if (sendFileUploadRequest == null) | ||
{ | ||
throw new ArgumentNullException(nameof(sendFileUploadRequest)); | ||
} | ||
|
||
if (string.IsNullOrEmpty(sendFileUploadRequest.FileUploadId)) | ||
{ | ||
throw new ArgumentNullException(nameof(sendFileUploadRequest.FileUploadId)); | ||
} | ||
|
||
if (sendFileUploadRequest.PartNumber != null) | ||
{ | ||
if (!int.TryParse(sendFileUploadRequest.PartNumber, out int partNumberValue) || partNumberValue < 1 || partNumberValue > 1000) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(sendFileUploadRequest.PartNumber), "PartNumber must be between 1 and 1000."); | ||
} | ||
} |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for sendFileUploadRequest.File and sendFileUploadRequest.File.Data can lead to a NullReferenceException later in RestClient when creating StreamContent; add explicit ArgumentNullException checks for File and File.Data before using them.
Copilot uses AI. Check for mistakes.
if (string.IsNullOrEmpty(sendFileUploadRequest.FileUploadId)) | ||
{ | ||
throw new ArgumentNullException(nameof(sendFileUploadRequest.FileUploadId)); | ||
} |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Using ArgumentNullException for an empty string is misleading; consider distinguishing null vs empty and throw ArgumentException for empty (or use string.IsNullOrWhiteSpace and ArgumentException) to better reflect the invalid value scenario.
if (string.IsNullOrEmpty(sendFileUploadRequest.FileUploadId)) | |
{ | |
throw new ArgumentNullException(nameof(sendFileUploadRequest.FileUploadId)); | |
} | |
if (sendFileUploadRequest.FileUploadId == null) | |
{ | |
throw new ArgumentNullException(nameof(sendFileUploadRequest.FileUploadId)); | |
} | |
if (sendFileUploadRequest.FileUploadId == string.Empty) | |
{ | |
throw new ArgumentException("FileUploadId cannot be empty.", nameof(sendFileUploadRequest.FileUploadId)); | |
} |
Copilot uses AI. Check for mistakes.
var sendRequest = SendFileUploadRequest.Create( | ||
createResponse.Id, | ||
new FileData | ||
{ | ||
FileName = "notion-logo.png", | ||
Data = System.IO.File.OpenRead("assets/notion-logo.png"), | ||
ContentType = createResponse.ContentType | ||
} | ||
); | ||
|
||
// Act | ||
var sendResponse = await Client.FileUploads.SendAsync(sendRequest); | ||
|
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The opened file stream is never disposed, risking a file handle leak; wrap the stream in a using statement (using var stream = File.OpenRead(...)) and pass the stream variable.
var sendRequest = SendFileUploadRequest.Create( | |
createResponse.Id, | |
new FileData | |
{ | |
FileName = "notion-logo.png", | |
Data = System.IO.File.OpenRead("assets/notion-logo.png"), | |
ContentType = createResponse.ContentType | |
} | |
); | |
// Act | |
var sendResponse = await Client.FileUploads.SendAsync(sendRequest); | |
using (var stream = System.IO.File.OpenRead("assets/notion-logo.png")) | |
{ | |
var sendRequest = SendFileUploadRequest.Create( | |
createResponse.Id, | |
new FileData | |
{ | |
FileName = "notion-logo.png", | |
Data = stream, | |
ContentType = createResponse.ContentType | |
} | |
); | |
// Act | |
var sendResponse = await Client.FileUploads.SendAsync(sendRequest); | |
// Assert | |
Assert.NotNull(sendResponse); | |
Assert.Equal(createResponse.Id, sendResponse.Id); | |
Assert.Equal("notion-logo.png", sendResponse.FileName); | |
Assert.Equal("uploaded", sendResponse.Status); | |
} |
Copilot uses AI. Check for mistakes.
@@ -25,5 +25,37 @@ public async Task CreateAsync() | |||
Assert.NotNull(response.Status); | |||
Assert.Equal("sample-image.jpg", response.FileName); | |||
} | |||
|
|||
[Fact] | |||
public async Task Verify_file_upload_flow() |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct file name typo: current file is named FIleUploadsClientTests.cs (uppercase I after F); should be FileUploadsClientTests.cs to match standard casing.
Copilot uses AI. Check for mistakes.
Task<T> PostAsync<T>( | ||
string uri, | ||
ISendFileUploadFormDataParameters formData, | ||
IEnumerable<KeyValuePair<string, string>> queryParams = null, | ||
IDictionary<string, string> headers = null, | ||
JsonSerializerSettings serializerSettings = null, | ||
IBasicAuthenticationParameters basicAuthenticationParameters = null, | ||
CancellationToken cancellationToken = default); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New overload lacks XML documentation differentiating it from the existing PostAsync (body JSON) overload; add summary and parameter docs clarifying it sends multipart/form-data with a file stream.
Copilot uses AI. Check for mistakes.
Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs
Outdated
Show resolved
Hide resolved
…t as Json to notion Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 9 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
throw new ArgumentOutOfRangeException(nameof(sendFileUploadRequest.PartNumber), "PartNumber must be between 1 and 1000."); | ||
} | ||
} | ||
|
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method validates FileUploadId and PartNumber but never validates sendFileUploadRequest.File (or its Data stream); a null File will cause a NullReferenceException later in RestClient.PostAsync when accessing formData.File.Data. Add a null check (and optionally ensure File.Data is not null) to throw a clear ArgumentNullException before invoking the REST call.
if (sendFileUploadRequest.File == null) | |
{ | |
throw new ArgumentNullException(nameof(sendFileUploadRequest.File), "File must not be null."); | |
} | |
if (sendFileUploadRequest.File.Data == null) | |
{ | |
throw new ArgumentNullException(nameof(sendFileUploadRequest.File.Data), "File.Data must not be null."); | |
} |
Copilot uses AI. Check for mistakes.
public async Task<T> PostAsync<T>( | ||
string uri, | ||
ISendFileUploadFormDataParameters formData, | ||
IEnumerable<KeyValuePair<string, string>> queryParams = null, | ||
IDictionary<string, string> headers = null, | ||
JsonSerializerSettings serializerSettings = null, | ||
IBasicAuthenticationParameters basicAuthenticationParameters = null, | ||
CancellationToken cancellationToken = default) |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The new PostAsync overload differs only by the second parameter type from the existing PostAsync(string, object, ...); calls with a null second argument (e.g., _restClient.PostAsync(uri, null)) will be ambiguous for consumers. Consider renaming this overload (e.g., PostMultipartAsync) or introducing a distinct request wrapper type to avoid overload ambiguity.
Copilot uses AI. Check for mistakes.
Task<T> PostAsync<T>( | ||
string uri, | ||
ISendFileUploadFormDataParameters formData, | ||
IEnumerable<KeyValuePair<string, string>> queryParams = null, | ||
IDictionary<string, string> headers = null, | ||
JsonSerializerSettings serializerSettings = null, | ||
IBasicAuthenticationParameters basicAuthenticationParameters = null, | ||
CancellationToken cancellationToken = default); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Mirroring the implementation, this overload creates potential ambiguity with the existing PostAsync signature when null is provided for formData; a differently named method (e.g., PostMultipartAsync) would make the API clearer and prevent ambiguous invocation errors.
Copilot uses AI. Check for mistakes.
using Newtonsoft.Json; | ||
|
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused using directive (Newtonsoft.Json) in this file; remove it to reduce unnecessary dependencies and keep the interface lean.
using Newtonsoft.Json; |
Copilot uses AI. Check for mistakes.
/// <summary> | ||
/// Send a file upload | ||
/// | ||
/// Requires a `file_upload_id`, obtained from the `id` of the Create File Upload API response. | ||
/// | ||
/// </summary> | ||
/// <param name="sendFileUploadRequest"></param> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns></returns> | ||
Task<SendFileUploadResponse> SendAsync( |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The summary omits important details (e.g., required File fields, behavior of optional PartNumber, constraints 1–1000, SinglePart vs MultiPart usage); expand the XML doc to describe expected request state and validation rules to aid consumers.
Copilot uses AI. Check for mistakes.
// Act & Assert | ||
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() => _fileUploadClient.CreateAsync(null)); | ||
Assert.Equal("fileUploadObjectRequest", exception.ParamName); | ||
Assert.Equal("Value cannot be null. (Parameter 'fileUploadObjectRequest')", exception.Message); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Asserting the full exception.Message is brittle across framework/localization changes; prefer asserting ParamName only (already done) or using Assert.StartsWith / Contains for the custom portion rather than the entire message.
Assert.Equal("Value cannot be null. (Parameter 'fileUploadObjectRequest')", exception.Message); | |
Assert.StartsWith("Value cannot be null", exception.Message); |
Copilot uses AI. Check for mistakes.
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() => _fileUploadClient.SendAsync(null)); | ||
Assert.Equal("sendFileUploadRequest", exception.ParamName); | ||
Assert.Equal("Value cannot be null. (Parameter 'sendFileUploadRequest')", exception.Message); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Full message equality makes the test fragile to runtime message format changes; restrict the assertion to ParamName (and optionally a Contains check) for resilience.
Copilot uses AI. Check for mistakes.
public async Task SendAsync_ThrowsArgumentNullException_WhenFileUploadIdIsNullOrEmpty() | ||
{ | ||
// Arrange | ||
var request = SendFileUploadRequest.Create(fileUploadId: null, file: new FileData { FileName = "testfile.txt", Data = new System.IO.MemoryStream(), ContentType = "text/plain" }); |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MemoryStream instances created for FileData (also at lines 100, 115, 152) are never disposed; wrap them in using statements or use a using var pattern to explicitly dispose and signal intent, even in tests.
Copilot uses AI. Check for mistakes.
|
||
var createResponse = await Client.FileUploads.CreateAsync(createRequest); | ||
|
||
using (var fileStream = File.OpenRead("assets/notion-logo.png")) |
Copilot
AI
Oct 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider specifying FileShare.Read (File.OpenRead defaults to FileShare.Read) or using File.Open with explicit modes for clarity; additionally, for large files asynchronous stream (FileOptions.Asynchronous) could improve throughput if awaited operations are extended.
using (var fileStream = File.OpenRead("assets/notion-logo.png")) | |
using (var fileStream = File.Open( | |
"assets/notion-logo.png", | |
FileMode.Open, | |
FileAccess.Read, | |
FileShare.Read, | |
4096, | |
FileOptions.Asynchronous)) |
Copilot uses AI. Check for mistakes.
Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes #478
Type of change
Please delete options that are not relevant.
Checklist: