From e0904189c101c6f9facd14faa5850707b6e8681a Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:41:39 +0530 Subject: [PATCH 01/12] Add support for send file upload api --- Src/Notion.Client/Api/ApiEndpoints.cs | 1 + .../Api/FileUploads/IFileUploadsClient.cs | 14 ++++ .../Api/FileUploads/Send/FileUploadsClient.cs | 32 +++++++++ .../Send/Request/SendFileUploadRequest.cs | 66 ++++++++++++++++++ .../Send/Response/SendFileUploadResponse.cs | 6 ++ Src/Notion.Client/RestClient/IRestClient.cs | 9 +++ Src/Notion.Client/RestClient/RestClient.cs | 40 +++++++++++ .../FIleUploadsClientTests.cs | 32 +++++++++ .../Notion.IntegrationTests.csproj | 6 ++ .../assets/notion-logo.png | Bin 0 -> 9339 bytes 10 files changed, 206 insertions(+) create mode 100644 Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs create mode 100644 Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs create mode 100644 Src/Notion.Client/Api/FileUploads/Send/Response/SendFileUploadResponse.cs create mode 100644 Test/Notion.IntegrationTests/assets/notion-logo.png diff --git a/Src/Notion.Client/Api/ApiEndpoints.cs b/Src/Notion.Client/Api/ApiEndpoints.cs index fea18493..9b3f663c 100644 --- a/Src/Notion.Client/Api/ApiEndpoints.cs +++ b/Src/Notion.Client/Api/ApiEndpoints.cs @@ -143,6 +143,7 @@ public static class AuthenticationUrls public static class FileUploadsApiUrls { public static string Create() => "/v1/file_uploads"; + public static string Send(string fileUploadId) => $"/v1/file_uploads/{fileUploadId}/send"; } } } diff --git a/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs index cca82a9f..48bd817b 100644 --- a/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs +++ b/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs @@ -15,5 +15,19 @@ Task CreateAsync( CreateFileUploadRequest fileUploadObjectRequest, CancellationToken cancellationToken = default ); + + /// + /// Send a file upload + /// + /// Requires a `file_upload_id`, obtained from the `id` of the Create File Upload API response. + /// + /// + /// + /// + /// + Task SendAsync( + SendFileUploadRequest sendFileUploadRequest, + CancellationToken cancellationToken = default + ); } } \ No newline at end of file diff --git a/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs new file mode 100644 index 00000000..00bbb7a9 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Notion.Client +{ + public sealed partial class FileUploadsClient + { + public async Task SendAsync( + SendFileUploadRequest sendFileUploadRequest, + CancellationToken cancellationToken = default) + { + if (sendFileUploadRequest == null) + { + throw new ArgumentNullException(nameof(sendFileUploadRequest)); + } + + if (string.IsNullOrEmpty(sendFileUploadRequest.FileUploadId)) + { + throw new ArgumentNullException(nameof(sendFileUploadRequest.FileUploadId)); + } + + var path = ApiEndpoints.FileUploadsApiUrls.Send(sendFileUploadRequest.FileUploadId); + + return await _restClient.PostAsync( + path, + formData: sendFileUploadRequest, + cancellationToken: cancellationToken + ); + } + } +} \ No newline at end of file diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs new file mode 100644 index 00000000..635cefdd --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs @@ -0,0 +1,66 @@ +using System.IO; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class SendFileUploadRequest : ISendFileUploadFormDataParameters, ISendFileUploadPathParameters + { + public FileData File { get; private set; } + public string PartNumber { get; private set; } + public string FileUploadId { get; private set; } + + private SendFileUploadRequest() { } + + public static SendFileUploadRequest Create(string fileUploadId, FileData file, string partNumber = null) + { + return new SendFileUploadRequest + { + FileUploadId = fileUploadId, + File = file, + PartNumber = partNumber + }; + } + } + + public interface ISendFileUploadFormDataParameters + { + + /// + /// The raw binary file contents to upload. + /// + FileData File { get; } + + /// + /// When using a mode=multi_part File Upload to send files greater than 20 MB in parts, this is the current part number. + /// Must be an integer between 1 and 1000 provided as a string form field. + /// + [JsonProperty("part_number")] + string PartNumber { get; } + } + + public class FileData + { + /// + /// The name of the file being uploaded. + /// + public string FileName { get; set; } + + /// + /// The content of the file being uploaded. + /// + public Stream Data { get; set; } + + /// + /// The MIME type of the file being uploaded. + /// + public string ContentType { get; set; } + } + + public interface ISendFileUploadPathParameters + { + /// + /// The `file_upload_id` obtained from the `id` of the Create File Upload API response. + /// + string FileUploadId { get; } + } +} \ No newline at end of file diff --git a/Src/Notion.Client/Api/FileUploads/Send/Response/SendFileUploadResponse.cs b/Src/Notion.Client/Api/FileUploads/Send/Response/SendFileUploadResponse.cs new file mode 100644 index 00000000..1061683d --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Send/Response/SendFileUploadResponse.cs @@ -0,0 +1,6 @@ +namespace Notion.Client +{ + public class SendFileUploadResponse : FileObjectResponse + { + } +} diff --git a/Src/Notion.Client/RestClient/IRestClient.cs b/Src/Notion.Client/RestClient/IRestClient.cs index 2e6fe960..dc288d91 100644 --- a/Src/Notion.Client/RestClient/IRestClient.cs +++ b/Src/Notion.Client/RestClient/IRestClient.cs @@ -23,6 +23,15 @@ Task PostAsync( IBasicAuthenticationParameters basicAuthenticationParameters = null, CancellationToken cancellationToken = default); + Task PostAsync( + string uri, + ISendFileUploadFormDataParameters formData, + IEnumerable> queryParams = null, + IDictionary headers = null, + JsonSerializerSettings serializerSettings = null, + IBasicAuthenticationParameters basicAuthenticationParameters = null, + CancellationToken cancellationToken = default); + Task PatchAsync( string uri, object body, diff --git a/Src/Notion.Client/RestClient/RestClient.cs b/Src/Notion.Client/RestClient/RestClient.cs index fc15f38d..a1b0aace 100644 --- a/Src/Notion.Client/RestClient/RestClient.cs +++ b/Src/Notion.Client/RestClient/RestClient.cs @@ -70,6 +70,46 @@ void AttachContent(HttpRequestMessage httpRequest) return await response.ParseStreamAsync(serializerSettings); } + public async Task PostAsync( + string uri, + ISendFileUploadFormDataParameters formData, + IEnumerable> queryParams = null, + IDictionary headers = null, + JsonSerializerSettings serializerSettings = null, + IBasicAuthenticationParameters basicAuthenticationParameters = null, + CancellationToken cancellationToken = default) + { + void AttachContent(HttpRequestMessage httpRequest) + { + var fileContent = new StreamContent(formData.File.Data); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(formData.File.ContentType); + + var form = new MultipartFormDataContent + { + { fileContent, "file", formData.File.FileName } + }; + + if (!string.IsNullOrEmpty(formData.PartNumber)) + { + form.Add(new StringContent(formData.PartNumber), "part_number"); + } + + httpRequest.Content = form; + } + + var response = await SendAsync( + uri, + HttpMethod.Post, + queryParams, + headers, + AttachContent, + basicAuthenticationParameters, + cancellationToken + ); + + return await response.ParseStreamAsync(serializerSettings); + } + public async Task PatchAsync( string uri, object body, diff --git a/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs b/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs index 306f550c..ffed56fe 100644 --- a/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs +++ b/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs @@ -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() + { + // Arrange + var createRequest = new CreateFileUploadRequest + { + Mode = FileUploadMode.SinglePart, + Filename = "notion-logo.png", + }; + + var createResponse = await Client.FileUploads.CreateAsync(createRequest); + + 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); + + // Assert + Assert.NotNull(sendResponse); + Assert.Equal(createResponse.Id, sendResponse.Id); + Assert.Equal("notion-logo.png", sendResponse.FileName); + Assert.Equal("uploaded", sendResponse.Status); + } } } \ No newline at end of file diff --git a/Test/Notion.IntegrationTests/Notion.IntegrationTests.csproj b/Test/Notion.IntegrationTests/Notion.IntegrationTests.csproj index 1284ee3f..65f2c257 100644 --- a/Test/Notion.IntegrationTests/Notion.IntegrationTests.csproj +++ b/Test/Notion.IntegrationTests/Notion.IntegrationTests.csproj @@ -11,6 +11,12 @@ + + + PreserveNewest + + + diff --git a/Test/Notion.IntegrationTests/assets/notion-logo.png b/Test/Notion.IntegrationTests/assets/notion-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6d04ee799a111085fbfe0749277a96de5bb693ad GIT binary patch literal 9339 zcmbt)2UHXL_U=p?EkH!TNXM>}69fgMDvBaVQ4}IAfG9;EO7DZHC`dUfVxcLbs7L}x zFBt`FR7Hx?f(WRz9FZa*{AUv0z2}~H-&^ag$0GBay=VWv@7rZ&7Fk@jG~X{QC?yC0 zfUwDdy@vq+3nQPe{8+@ZY4+3)WcgxgX1xzt|9^f+B$9%Hf}x?IrKKf}M)URcg&-(1 zGn2t!uvo1A{(cIDlAN5}($X?HIk~*N?BwK>l$3-pMny%@>GZ6utp5x9($Z32U*E*U zL}FrMO-)U6b8|*U29wFOu&}VQvf90ScYc1pnwpxTqT;Gmt3VJubLNb*v$MImxq*Sf z-(g(<`*_4Q^FM5U1RyC;3~9blCaa}?a+#5A=@gYjM}+<3CkwIrmhuxPnrkvF{&9_K zIvR}o+*~-I4*=`tP4@1#4hF}1`L2GZh~q4^?ZO)}{Fk5jIBxS%kP@Ek-QE7cZsU8o zERV`a@=K4~BibWI;YHW8H?BMDIN~kkIKLCD^g=E@3~7$FeQUCg(Po+oReE+uk{@_G z`Y8{�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 W@ByB^JJJ8PG1+InH-8W9-2VVuPVJlk literal 0 HcmV?d00001 From 5f36bbf53d0b403fe5f11db58bc8712ef6ce6f2d Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:46:26 +0530 Subject: [PATCH 02/12] Fix code factor warnings --- .../Api/FileUploads/Send/Request/FileData.cs | 22 +++++++++ .../ISendFileUploadFormDataParameters.cs | 20 ++++++++ .../Request/ISendFileUploadPathParameters.cs | 10 ++++ .../Send/Request/SendFileUploadRequest.cs | 47 +------------------ 4 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 Src/Notion.Client/Api/FileUploads/Send/Request/FileData.cs create mode 100644 Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs create mode 100644 Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadPathParameters.cs diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/FileData.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/FileData.cs new file mode 100644 index 00000000..a0825e0b --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/FileData.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace Notion.Client +{ + public class FileData + { + /// + /// The name of the file being uploaded. + /// + public string FileName { get; set; } + + /// + /// The content of the file being uploaded. + /// + public Stream Data { get; set; } + + /// + /// The MIME type of the file being uploaded. + /// + public string ContentType { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs new file mode 100644 index 00000000..ec7f3031 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Notion.Client +{ + public interface ISendFileUploadFormDataParameters + { + + /// + /// The raw binary file contents to upload. + /// + FileData File { get; } + + /// + /// When using a mode=multi_part File Upload to send files greater than 20 MB in parts, this is the current part number. + /// Must be an integer between 1 and 1000 provided as a string form field. + /// + [JsonProperty("part_number")] + string PartNumber { get; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadPathParameters.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadPathParameters.cs new file mode 100644 index 00000000..53290575 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadPathParameters.cs @@ -0,0 +1,10 @@ +namespace Notion.Client +{ + public interface ISendFileUploadPathParameters + { + /// + /// The `file_upload_id` obtained from the `id` of the Create File Upload API response. + /// + string FileUploadId { get; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs index 635cefdd..0e7c8b01 100644 --- a/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/SendFileUploadRequest.cs @@ -1,6 +1,3 @@ -using System.IO; -using Newtonsoft.Json; - namespace Notion.Client { public class SendFileUploadRequest : ISendFileUploadFormDataParameters, ISendFileUploadPathParameters @@ -21,46 +18,4 @@ public static SendFileUploadRequest Create(string fileUploadId, FileData file, s }; } } - - public interface ISendFileUploadFormDataParameters - { - - /// - /// The raw binary file contents to upload. - /// - FileData File { get; } - - /// - /// When using a mode=multi_part File Upload to send files greater than 20 MB in parts, this is the current part number. - /// Must be an integer between 1 and 1000 provided as a string form field. - /// - [JsonProperty("part_number")] - string PartNumber { get; } - } - - public class FileData - { - /// - /// The name of the file being uploaded. - /// - public string FileName { get; set; } - - /// - /// The content of the file being uploaded. - /// - public Stream Data { get; set; } - - /// - /// The MIME type of the file being uploaded. - /// - public string ContentType { get; set; } - } - - public interface ISendFileUploadPathParameters - { - /// - /// The `file_upload_id` obtained from the `id` of the Create File Upload API response. - /// - string FileUploadId { get; } - } -} \ No newline at end of file +} From c58e2ab8f4f9972d329eb8acb395dba40b857969 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 17:45:30 +0530 Subject: [PATCH 03/12] Fix code factor warnings --- .../Send/Request/ISendFileUploadFormDataParameters.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs index ec7f3031..433a07ad 100644 --- a/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs @@ -1,10 +1,9 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Notion.Client { public interface ISendFileUploadFormDataParameters { - /// /// The raw binary file contents to upload. /// From e48333f66286bec06d49ed7240aa78ba377ac722 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:35:32 +0530 Subject: [PATCH 04/12] Adjust property name in integration test --- Test/Notion.IntegrationTests/FIleUploadsClientTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs b/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs index 4b8f6ead..392685f8 100644 --- a/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs +++ b/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs @@ -33,7 +33,7 @@ public async Task Verify_file_upload_flow() var createRequest = new CreateFileUploadRequest { Mode = FileUploadMode.SinglePart, - Filename = "notion-logo.png", + FileName = "notion-logo.png", }; var createResponse = await Client.FileUploads.CreateAsync(createRequest); From eed81b47b7cfd92aebb78b804554b301cb0c0066 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:58:29 +0530 Subject: [PATCH 05/12] Add validation for part number if provided should be between 1 to 1000 --- .../Api/FileUploads/Send/FileUploadsClient.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs index 00bbb7a9..91685164 100644 --- a/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs +++ b/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs @@ -20,6 +20,14 @@ public async Task SendAsync( 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."); + } + } + var path = ApiEndpoints.FileUploadsApiUrls.Send(sendFileUploadRequest.FileUploadId); return await _restClient.PostAsync( From 8254b8bde71bcf1de8c6376bbb87082d8d3f47a9 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:58:50 +0530 Subject: [PATCH 06/12] Add unit tests for Send file upload api endpoint --- .../Notion.UnitTests/FileUploadClientTests.cs | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/Test/Notion.UnitTests/FileUploadClientTests.cs b/Test/Notion.UnitTests/FileUploadClientTests.cs index 53ebaa59..cf62c073 100644 --- a/Test/Notion.UnitTests/FileUploadClientTests.cs +++ b/Test/Notion.UnitTests/FileUploadClientTests.cs @@ -67,4 +67,115 @@ public async Task CreateAsync_CallsRestClientPostAsync_WithCorrectParameters() _restClientMock.VerifyAll(); } -} \ No newline at end of file + + [Fact] + public async Task SendAsync_ThrowsArgumentNullException_WhenRequestIsNull() + { + // Act & Assert + var exception = await Assert.ThrowsAsync(() => _fileUploadClient.SendAsync(null)); + Assert.Equal("sendFileUploadRequest", exception.ParamName); + Assert.Equal("Value cannot be null. (Parameter 'sendFileUploadRequest')", exception.Message); + } + + [Fact] + 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" }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => _fileUploadClient.SendAsync(request)); + Assert.Equal("FileUploadId", exception.ParamName); + Assert.Equal("Value cannot be null. (Parameter 'FileUploadId')", exception.Message); + } + + [Theory] + [InlineData("0")] + [InlineData("1001")] + [InlineData("-5")] + [InlineData("abc")] + public async Task SendAsync_ThrowsArgumentOutOfRangeException_WhenPartNumberIsInvalid(string partNumber) + { + // Arrange + var request = SendFileUploadRequest.Create(fileUploadId: "valid-id", file: new FileData { FileName = "testfile.txt", Data = new System.IO.MemoryStream(), ContentType = "text/plain" }, partNumber: partNumber); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => _fileUploadClient.SendAsync(request)); + Assert.Equal("PartNumber", exception.ParamName); + Assert.Contains("PartNumber must be between 1 and 1000.", exception.Message); + } + + [Theory] + [InlineData("1")] + [InlineData("500")] + [InlineData("1000")] + public async Task SendAsync_DoesNotThrow_WhenPartNumberIsValid(string partNumber) + { + // Arrange + var request = SendFileUploadRequest.Create(fileUploadId: "valid-id", file: new FileData { FileName = "testfile.txt", Data = new System.IO.MemoryStream(), ContentType = "text/plain" }, partNumber: partNumber); + + var expectedResponse = new SendFileUploadResponse + { + Id = "valid-id", + Status = "uploaded", + }; + + _restClientMock + .Setup(client => client.PostAsync( + It.Is(url => url == ApiEndpoints.FileUploadsApiUrls.Send("valid-id")), + It.IsAny(), + It.IsAny>>(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var exception = await Record.ExceptionAsync(() => _fileUploadClient.SendAsync(request)); + + // Assert + Assert.Null(exception); + _restClientMock.VerifyAll(); + } + + [Fact] + public async Task SendAsync_CallsRestClientPostAsync_WithCorrectParameters() + { + // Arrange + var fileUploadId = Guid.NewGuid().ToString(); + var request = SendFileUploadRequest.Create( + fileUploadId: fileUploadId, + file: new FileData + { + FileName = "testfile.txt", + Data = new System.IO.MemoryStream(), + ContentType = "text/plain" + } + ); + + var expectedResponse = new SendFileUploadResponse + { + Id = fileUploadId.ToString(), + Status = "uploaded", + }; + + _restClientMock + .Setup(client => client.PostAsync( + It.Is(url => url == ApiEndpoints.FileUploadsApiUrls.Send(fileUploadId)), + It.IsAny(), + It.IsAny>>(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(expectedResponse); + // Act + var response = await _fileUploadClient.SendAsync(request); + + // Assert + Assert.Equal(expectedResponse.Status, response.Status); + Assert.Equal(expectedResponse.Id, response.Id); + _restClientMock.VerifyAll(); + } +} From 204478fb79086afc24f82a90a4063e2187303b4f Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:19:01 +0530 Subject: [PATCH 07/12] Remove JsonProperty attribute from PartNumber as the model is not sent as Json to notion Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Send/Request/ISendFileUploadFormDataParameters.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs index 433a07ad..c4fe2b74 100644 --- a/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs +++ b/Src/Notion.Client/Api/FileUploads/Send/Request/ISendFileUploadFormDataParameters.cs @@ -13,7 +13,6 @@ public interface ISendFileUploadFormDataParameters /// When using a mode=multi_part File Upload to send files greater than 20 MB in parts, this is the current part number. /// Must be an integer between 1 and 1000 provided as a string form field. /// - [JsonProperty("part_number")] string PartNumber { get; } } } From 33ea73537760c4343abba3e55d44169dcb01fa8f Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:25:38 +0530 Subject: [PATCH 08/12] Rename file to FileUploadsClientTests --- .../{FIleUploadsClientTests.cs => FileUploadClientTests.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Test/Notion.IntegrationTests/{FIleUploadsClientTests.cs => FileUploadClientTests.cs} (100%) diff --git a/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs b/Test/Notion.IntegrationTests/FileUploadClientTests.cs similarity index 100% rename from Test/Notion.IntegrationTests/FIleUploadsClientTests.cs rename to Test/Notion.IntegrationTests/FileUploadClientTests.cs From 8766e0ede144bb27f7c714372f6f83aa962b9fb1 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:26:25 +0530 Subject: [PATCH 09/12] Rename test file to FileUploadsClientTests --- .../{FileUploadClientTests.cs => FileUploadsClientTests.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Test/Notion.IntegrationTests/{FileUploadClientTests.cs => FileUploadsClientTests.cs} (100%) diff --git a/Test/Notion.IntegrationTests/FileUploadClientTests.cs b/Test/Notion.IntegrationTests/FileUploadsClientTests.cs similarity index 100% rename from Test/Notion.IntegrationTests/FileUploadClientTests.cs rename to Test/Notion.IntegrationTests/FileUploadsClientTests.cs From 95a2f9335bb3d49f7bc032005d999243f76a3a6f Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:27:23 +0530 Subject: [PATCH 10/12] Rename unit test file to FileUploadsClientTests --- .../{FileUploadClientTests.cs => FileUploadsClientTests.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename Test/Notion.UnitTests/{FileUploadClientTests.cs => FileUploadsClientTests.cs} (98%) diff --git a/Test/Notion.UnitTests/FileUploadClientTests.cs b/Test/Notion.UnitTests/FileUploadsClientTests.cs similarity index 98% rename from Test/Notion.UnitTests/FileUploadClientTests.cs rename to Test/Notion.UnitTests/FileUploadsClientTests.cs index cf62c073..d1f0c3a7 100644 --- a/Test/Notion.UnitTests/FileUploadClientTests.cs +++ b/Test/Notion.UnitTests/FileUploadsClientTests.cs @@ -10,13 +10,13 @@ namespace Notion.UnitTests; -public class FileUploadClientTests +public class FileUploadsClientTests { private readonly AutoMocker _mocker = new(); private readonly FileUploadsClient _fileUploadClient; private readonly Mock _restClientMock; - public FileUploadClientTests() + public FileUploadsClientTests() { _restClientMock = _mocker.GetMock(); _fileUploadClient = _mocker.CreateInstance(); From 66fb939d2e856dfb1573587396585589da368b17 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:30:40 +0530 Subject: [PATCH 11/12] Dispose stream to avoid potential leak --- .../FileUploadsClientTests.cs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Test/Notion.IntegrationTests/FileUploadsClientTests.cs b/Test/Notion.IntegrationTests/FileUploadsClientTests.cs index 392685f8..ca7e73ef 100644 --- a/Test/Notion.IntegrationTests/FileUploadsClientTests.cs +++ b/Test/Notion.IntegrationTests/FileUploadsClientTests.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Threading.Tasks; using Notion.Client; using Xunit; @@ -38,24 +39,27 @@ public async Task Verify_file_upload_flow() var createResponse = await Client.FileUploads.CreateAsync(createRequest); - var sendRequest = SendFileUploadRequest.Create( - createResponse.Id, - new FileData - { - FileName = "notion-logo.png", - Data = System.IO.File.OpenRead("assets/notion-logo.png"), - ContentType = createResponse.ContentType - } - ); + using (var fileStream = File.OpenRead("assets/notion-logo.png")) + { + var sendRequest = SendFileUploadRequest.Create( + createResponse.Id, + new FileData + { + FileName = "notion-logo.png", + Data = fileStream, + ContentType = createResponse.ContentType + } + ); - // Act - var sendResponse = await Client.FileUploads.SendAsync(sendRequest); + // 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); + // Assert + Assert.NotNull(sendResponse); + Assert.Equal(createResponse.Id, sendResponse.Id); + Assert.Equal("notion-logo.png", sendResponse.FileName); + Assert.Equal("uploaded", sendResponse.Status); + } } } } \ No newline at end of file From fd6c4fbbd3d8f263627b99e9f26506e2f9c3643c Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:32:37 +0530 Subject: [PATCH 12/12] Use IsNullOrWhiteSpace to check file upload id is present --- Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs index 91685164..73a4de79 100644 --- a/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs +++ b/Src/Notion.Client/Api/FileUploads/Send/FileUploadsClient.cs @@ -15,7 +15,7 @@ public async Task SendAsync( throw new ArgumentNullException(nameof(sendFileUploadRequest)); } - if (string.IsNullOrEmpty(sendFileUploadRequest.FileUploadId)) + if (string.IsNullOrWhiteSpace(sendFileUploadRequest.FileUploadId)) { throw new ArgumentNullException(nameof(sendFileUploadRequest.FileUploadId)); }