Skip to content

Commit 41aaf01

Browse files
mm-omdimm-kgiCopilotmm-hshmm-asha
authored
File attechment submodel (#193)
* Enhance Shell Retrieval with IdShort Filtering * Refactor shell retrieval endpoints to support idShort filtering and enhance asset ID queries * Added the implementation of get all submodels endpoint * Added the level and extent as query param for specific submodel endpoint * remove unnecessary cast to 'IEnumerable<ISubmodel>'. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Refactor and add unit tests for submodel service Refactor the return statement in `SubmodelRepositoryService.cs` to use the C# 8.0 range operator for improved collection handling. Add comprehensive unit tests in `GetAllSubmodelsControllerTests.cs` to cover various scenarios for the `GetAllSubmodelsAsync` method, including handling of query parameters, paging, and exception cases. Utilize `NSubstitute` for mocking and `Xunit` for test structuring. * Remove unused ContactInformation template mapping from appsettings.development.json * Enhance SubmodelRepositoryService to support concurrent operations with semaphore and add configuration for concurrent operations limit * Add TemplateManagementConfig to SubmodelRepositoryService Introduce `_templateManagementOptions` in `SubmodelRepositoryServiceTests` to configure `TemplateManagementConfig` with a `ServiceInstance` and `ConcurrentOperationsLimit`. Update the constructor of `SubmodelRepositoryService` to include the new parameter. Add necessary using directives for configuration and options handling. * Add TemplateManagementConfig options to SubmodelRepositoryServiceTests * Add ConcurrentOperationsLimit to service instances and validate configuration * Sanitize URLs in logging for HTTP requests in TemplateProvider * Refactor SubmodelRepositoryController and related tests to enhance GetAllSubmodelsAsync functionality and improve request handling * Add GetAllSubmodelsControllerTests to validate GetAllSubmodelsAsync functionality * Update SubmodelRepositoryControllerTests to use GetFilteredSubmodelTemplateAsync and adjust request paths * Refactor SubmodelTemplateService and related tests to use GetFilteredSubmodelTemplateAsync for improved query handling * Refactor various services to improve code clarity and consistency, including adjustments to semaphore release and data structure initialization. * Add Get All Submodels - by idShort endpoint definition * refactor the template provider tests * Implement Get All Submodel Elements endpoint with associated request and response models, and add unit and integration tests * Refactor query parameter handling in GetAllSubmodelElementsControllerTests for improved readability * merge parent branch * Implement GetFileAttachment functionality with associated tests and configuration * Add file size validation and exception handling for file attachments * Remove unused GetAllSubmodelElements method and SubmodelElementsDto mapping in SubmodelsMapperProfile * Add file attachment handling and tracing to SubmodelRepositoryService * Refactor file attachment handling in SubmodelRepositoryService to redirect URLs and remove unused code * Add unit tests for GetFileAttachmentAsync in SubmodelRepositoryServiceTests * Refactor file attachment handling to remove return types and simplify methods; delete unused configuration classes. * Enhance file attachment handling by updating method signatures to return FileAttachmentResult; implement streaming timeout configuration; improve unit tests for file attachment retrieval. * Refactor file attachment handling: update SubmodelRepositoryHandler and Service to support max file size; add unit tests for GetFileAttachment; adjust configuration settings. * Implement file attachment streaming: add IFileAttachmentStreamProvider interface and FileAttachmentStreamProvider class; refactor SubmodelRepositoryService and tests to utilize new provider. * Refactor file attachment handling: introduce TemplateServices and PluginServices for better dependency management; add FileSizeExceededException and InvalidFileUrlException for improved error handling; update appsettings.json to configure max file attachment size. * Refactor file attachment handling: update exception types in GetFileAttachmentAsync method; improve null handling for submodel ID; enhance file size validation logic in SubmodelRepositoryService; adjust HTTP client configuration for plugins. * Refactor file attachment handling: update documentation for file download endpoint; improve response descriptions; correct max file attachment size in configuration files. * Updated API collections in both main and test plugin directories * Add MaxLengthStream class for file size enforcement in streaming * Add unit tests for FileAttachmentStreamProvider and MaxLengthStream - Implemented tests for FileAttachmentStreamProvider to verify response headers and content stream reading. - Added tests for MaxLengthStream to ensure it correctly enforces maximum byte limits and throws exceptions when exceeded. - Refactored SubmodelRepositoryHandler to streamline file attachment retrieval logic. - Updated FileSizeExceededException and InvalidFileUrlException to simplify constructors and messages. - Introduced MaxLengthStream class to limit stream reading to a specified byte size. - Modified SubmodelRepositoryService to enhance file URL validation and response handling. - Updated JSON test data to remove Google Docs viewer URLs in favor of direct file URLs. - Adjusted SQL load script to reflect changes in document URL handling. * Enhance error logging in SubmodelRepositoryService: added logging for invalid file element types and empty file URLs. * Refactor file attachment handling: introduce IFileContentProvider interface and FileContentProvider class; update SubmodelRepositoryService to use new provider; add unit tests for MaxLengthStream and FileContentProvider. * Refactor SubmodelRepositoryControllerTests to use IFileContentProvider; update exception classes by removing unnecessary comments and whitespace. * File attachment review findings and discussion * Implement file attachment handling improvements: add LimitedFileStreamResult, FileAttachmentResult, and FileContentResponse classes; refactor SubmodelRepositoryService and related components; update exception handling for file size limits; remove obsolete exceptions. * Refactor file attachment handling in tests: update SubmodelRepositoryControllerTests and SubmodelRepositoryHandlerTests to use new FileContentResponse and LimitedFileStreamResult; adjust assertions and exception handling for improved clarity and accuracy. * Refactor file attachment handling: update GetFileAttachmentAsync method to return FileStreamResult instead of LimitedFileStreamResult; modify related unit tests for consistency and clarity. * Refactor to use FileContentStreamResult for attachments Renamed FileStreamResult to FileContentStreamResult and updated all references, including controller logic and tests, to ensure consistent handling of file attachments with the new result type. * Refactor error handling in GetFileContentAsync method: replace EnsureSuccessStatusCode with explicit check for success status and throw InternalDataProcessingException on failure. * Refactor file attachment handling: remove commented-out sections in tests and update exception handling to throw InvalidUserInputException with specific messages. * updated module test * refactor the exceptions handling in the submodel repository service * Refactor GlobalExceptionHandler: streamline error handling by consolidating status code and message retrieval into GetErrorDetails method. * Refactor GetErrorDetails method: streamline exception handling by consolidating return statements for clarity. * Refactor FileContentResponse: simplify constructor by removing contentLength and contentType parameters; update related usages in FileContentProvider and tests. * Removed unused variable --------- Co-authored-by: Kevalkumar <kgi@mm-software.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Hardi Shah <hsh@mm-software.com> Co-authored-by: Ankit Shah <ash@mm-software.com>
1 parent c6db615 commit 41aaf01

44 files changed

Lines changed: 842 additions & 151 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
meta {
2+
name: Get Submodel Element File Attachment - HandoverDocumentation
3+
type: http
4+
seq: 5
5+
}
6+
7+
get {
8+
url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath/attachment
9+
body: none
10+
auth: inherit
11+
}
12+
13+
params:path {
14+
submodelIdentifier: {{submodelIdentifierHandoverDocumentation-1}}
15+
idShortPath: Documents[0].DocumentVersions[0].DigitalFiles[0]
16+
}
17+
18+
settings {
19+
encodeUrl: true
20+
timeout: 0
21+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
meta {
2+
name: Submodel File Attachment
3+
}

example/postgres/seed/01_core_asset_tables.sql.inc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ TRUE,
5252
5,
5353
'2025-12-24T14:30:00Z',
5454
'2035-12-24T14:30:00Z',
55-
('https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/' ||
55+
('https://raw.githubusercontent.com/AAS-TwinEngine/' ||
5656
'AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf')
5757
),
5858

@@ -98,7 +98,7 @@ FALSE,
9898
2.2999999999999998,
9999
'2026-01-15T09:15:00+05:30',
100100
'2036-01-15T09:15:00+05:31',
101-
('https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/' ||
101+
('https://raw.githubusercontent.com/AAS-TwinEngine/' ||
102102
'AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf')
103103
),
104104

@@ -144,7 +144,7 @@ TRUE,
144144
7.8,
145145
'2024-07-01T18:45:00-04:00',
146146
'2034-07-01T18:45:00-04:01',
147-
('https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/' ||
147+
('https://raw.githubusercontent.com/AAS-TwinEngine/' ||
148148
'AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf')
149149
);
150150

example/postgres/seed/04_handoverdocumentation.sql.inc

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ INSERT INTO "DocumentVersion" (
8787
) VALUES
8888
(
8989
0,
90-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
90+
'https://raw.githubusercontent.com/'
9191
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
9292
'1',
9393
'2023-01-01',
@@ -108,7 +108,7 @@ INSERT INTO "DocumentVersion" (
108108

109109
(
110110
1,
111-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
111+
'https://raw.githubusercontent.com/'
112112
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
113113
'1.1',
114114
'2024-05-05',
@@ -129,7 +129,7 @@ INSERT INTO "DocumentVersion" (
129129

130130
(
131131
0,
132-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
132+
'https://raw.githubusercontent.com/'
133133
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
134134
'2.1',
135135
'2026-01-01',
@@ -150,7 +150,7 @@ INSERT INTO "DocumentVersion" (
150150

151151
(
152152
0,
153-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
153+
'https://raw.githubusercontent.com/'
154154
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
155155
'2.3',
156156
'2025-10-10',
@@ -171,7 +171,7 @@ INSERT INTO "DocumentVersion" (
171171

172172
(
173173
0,
174-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
174+
'https://raw.githubusercontent.com/'
175175
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
176176
'0.9',
177177
'2024-01-01',
@@ -192,7 +192,7 @@ INSERT INTO "DocumentVersion" (
192192

193193
(
194194
0,
195-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
195+
'https://raw.githubusercontent.com/'
196196
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
197197
'1.2',
198198
'2024-03-03',
@@ -213,7 +213,7 @@ INSERT INTO "DocumentVersion" (
213213

214214
(
215215
1,
216-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
216+
'https://raw.githubusercontent.com/'
217217
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
218218
'1.4',
219219
'2023-01-01',
@@ -234,7 +234,7 @@ INSERT INTO "DocumentVersion" (
234234

235235
(
236236
2,
237-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
237+
'https://raw.githubusercontent.com/'
238238
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
239239
'2',
240240
'2024-03-03',
@@ -255,7 +255,7 @@ INSERT INTO "DocumentVersion" (
255255

256256
(
257257
0,
258-
'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/'
258+
'https://raw.githubusercontent.com/'
259259
'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf',
260260
'1',
261261
'2022-02-02',

source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net;
2+
using System.Net.Http.Headers;
23
using System.Net.Http.Json;
34
using System.Text;
45
using System.Text.Json.Nodes;
@@ -7,6 +8,7 @@
78
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers;
89
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin;
910
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers;
11+
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Providers;
1012
using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients;
1113
using AAS.TwinEngine.DataEngine.ModuleTests.Common;
1214

@@ -18,6 +20,7 @@
1820

1921
using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config;
2022
using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository;
23+
using AAS.TwinEngine.DataEngine.DomainModel.Shared;
2124

2225
namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRepository;
2326

@@ -27,20 +30,23 @@ public abstract class SubmodelRepositoryControllerTests : IDisposable
2730
private readonly ITemplateProvider _mockTemplateProvider;
2831
private readonly HttpClient _client;
2932
private readonly ICreateClient _httpClientFactory;
33+
private readonly IFileContentProvider _fileContentProvider;
3034

3135
protected SubmodelRepositoryControllerTests(string configDir)
3236
{
3337
_mockTemplateProvider = Substitute.For<ITemplateProvider>();
3438
var mockPluginManifestProvider = Substitute.For<IPluginManifestProvider>();
3539
var mockPluginManifestConflictHandler = Substitute.For<IPluginManifestConflictHandler>();
3640
_httpClientFactory = Substitute.For<ICreateClient>();
41+
_fileContentProvider = Substitute.For<IFileContentProvider>();
3742

3843
_factory = new ConfigTestFactory(configDir, services =>
3944
{
4045
_ = services.AddSingleton(_httpClientFactory);
4146
_ = services.AddSingleton(_mockTemplateProvider);
4247
_ = services.AddSingleton(mockPluginManifestProvider);
4348
_ = services.AddSingleton(mockPluginManifestConflictHandler);
49+
_ = services.AddSingleton(_fileContentProvider);
4450
});
4551

4652
_client = _factory.CreateClient();
@@ -180,6 +186,72 @@ public async Task GetSubmodelElementAsync_WithNotFound_Returns404Async()
180186
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
181187
}
182188

189+
[Fact]
190+
public async Task GetFileAttachmentAsync_WhenElementIsFileWithHttpUrl_StreamsContentAsync()
191+
{
192+
// Arrange
193+
const string SubmodelId = "Q29udGFjdEluZm9ybWF0aW9u";
194+
const string IdShortPath = "Thumbnail";
195+
const string FileUrl = "https://example.com/logo.png";
196+
var fileBytes = Encoding.UTF8.GetBytes("fake-image-bytes");
197+
198+
using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage
199+
{
200+
StatusCode = HttpStatusCode.OK,
201+
Content = new StringContent(TestData.CreatePluginResponseForFileAttachment())
202+
}));
203+
204+
using var httpClient = new HttpClient(messageHandler);
205+
httpClient.BaseAddress = new Uri("https://testendpoint.com");
206+
207+
const string HttpClientName = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1";
208+
_ = _httpClientFactory.CreateClient(HttpClientName).Returns(httpClient);
209+
210+
_ = _mockTemplateProvider.GetFilteredSubmodelTemplateAsync(Arg.Any<string>(), Arg.Any<SubmodelQueryOptions?>(), Arg.Any<CancellationToken>()).Returns(TestData.CreateSubmodel());
211+
212+
var fileContentResponse = new FileContentResponse(new MemoryStream(fileBytes));
213+
_ = _fileContentProvider.GetFileContentAsync(FileUrl, Arg.Any<CancellationToken>()).Returns(fileContentResponse);
214+
215+
// Act
216+
var response = await _client.GetAsync($"/submodels/{SubmodelId}/submodel-elements/{IdShortPath}/attachment");
217+
218+
// Assert
219+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
220+
Assert.Equal("image/png", response.Content.Headers.ContentType?.MediaType);
221+
var body = await response.Content.ReadAsByteArrayAsync();
222+
Assert.Equal(fileBytes, body);
223+
Assert.Contains("logo.png", response.Content.Headers.ContentDisposition?.ToString(), StringComparison.Ordinal);
224+
await _fileContentProvider.Received(1).GetFileContentAsync(FileUrl, Arg.Any<CancellationToken>());
225+
}
226+
227+
[Fact]
228+
public async Task GetFileAttachmentAsync_WhenElementIsNotFile_Returns400Async()
229+
{
230+
// Arrange
231+
const string SubmodelId = "Q29udGFjdEluZm9ybWF0aW9u";
232+
const string IdShortPath = "ContactName";
233+
234+
using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage
235+
{
236+
StatusCode = HttpStatusCode.OK,
237+
Content = new StringContent(TestData.CreatePluginResponseForSubmodelElement())
238+
}));
239+
240+
using var httpClient = new HttpClient(messageHandler);
241+
httpClient.BaseAddress = new Uri("https://testendpoint.com");
242+
243+
const string HttpClientName = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1";
244+
_ = _httpClientFactory.CreateClient(HttpClientName).Returns(httpClient);
245+
246+
_ = _mockTemplateProvider.GetFilteredSubmodelTemplateAsync(Arg.Any<string>(), Arg.Any<SubmodelQueryOptions?>(), Arg.Any<CancellationToken>()).Returns(TestData.CreateSubmodel());
247+
248+
// Act
249+
var response = await _client.GetAsync($"/submodels/{SubmodelId}/submodel-elements/{IdShortPath}/attachment");
250+
251+
// Assert
252+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
253+
}
254+
183255
[Fact]
184256
public async Task GetSubmodelElementAsync_WhenIdentifierIsInValid_Returns400Async()
185257
{

source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/TestData.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ public static string CreatePluginResponseForSubmodelElement() => """
144144
}
145145
""";
146146

147+
public static string CreatePluginResponseForFileAttachment() => """
148+
{
149+
"http://example.com/idta/digital-nameplate/semantic-id": {
150+
"http://example.com/idta/digital-nameplate/thumbnail": "https://example.com/logo.png"
151+
}
152+
}
153+
""";
154+
147155
public static string CreatePlugin1ResponseForSubmodel() => """
148156
{
149157
"http://example.com/idta/digital-nameplate/semantic-id": {

source/AAS.TwinEngine.DataEngine.UnitTests/Api/SubmodelRepository/Handler/SubmodelRepositoryHandlerTests.cs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Text;
1+
using System.Text;
22

33
using AAS.TwinEngine.DataEngine.Api.SubmodelRepository.Handler;
44
using AAS.TwinEngine.DataEngine.Api.SubmodelRepository.Requests;
55
using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application;
66
using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions;
77
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository;
8+
using AAS.TwinEngine.DataEngine.DomainModel.Shared;
89
using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository;
910

1011
using AasCore.Aas3_1;
@@ -15,13 +16,17 @@
1516
using NSubstitute;
1617

1718
namespace AAS.TwinEngine.DataEngine.UnitTests.Api.SubmodelRepository.Handler;
19+
1820
public class SubmodelRepositoryHandlerTests
1921
{
2022
private readonly ISubmodelRepositoryService _submodelRepository = Substitute.For<ISubmodelRepositoryService>();
2123
private readonly ILogger<SubmodelRepositoryHandler> _logger = Substitute.For<ILogger<SubmodelRepositoryHandler>>();
2224
private readonly SubmodelRepositoryHandler _sut;
2325

24-
public SubmodelRepositoryHandlerTests() => _sut = new SubmodelRepositoryHandler(_logger, _submodelRepository);
26+
public SubmodelRepositoryHandlerTests()
27+
{
28+
_sut = new SubmodelRepositoryHandler(_logger, _submodelRepository);
29+
}
2530

2631
[Fact]
2732
public async Task HandleSubmodel_ReturnsSubmodel_WhenSubmodelExists()
@@ -344,7 +349,7 @@ await _submodelRepository.Received(1).GetAllSubmodelsAsync(
344349
[Fact]
345350
public async Task GetAllSubmodels_WhenNullRequest_ReturnsSuccessfully()
346351
{
347-
var submodelList = new SubmodelList { PagingMetaData = new DomainModel.Shared.PagingMetaData(), Result = [] };
352+
var submodelList = new SubmodelList { PagingMetaData = new PagingMetaData(), Result = [] };
348353
_submodelRepository.GetAllSubmodelsAsync(Arg.Any<SubmodelSearchFilter?>(), Arg.Any<SubmodelQueryOptions?>(), null, null, Arg.Any<CancellationToken>())
349354
.Returns(submodelList);
350355

@@ -452,4 +457,62 @@ await _submodelRepository.Received(1)
452457
.GetAllSubmodelElementsAsync(SubmodelId, null, null, null, Arg.Any<CancellationToken>());
453458
}
454459

460+
[Fact]
461+
public async Task GetFileAttachment_CallsService_WhenServiceSucceeds()
462+
{
463+
const string SubmodelId = "NameplateSubmodel";
464+
const string IdShortPath = "Documents.ProductImage";
465+
var encodedId = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(SubmodelId));
466+
var request = new GetSubmodelElementRequest(encodedId, IdShortPath);
467+
var fakeResult = new FileAttachmentResult(Stream.Null, "application/octet-stream", "file.bin", 100 * 1024 * 1024);
468+
_submodelRepository
469+
.GetFileAttachmentAsync(SubmodelId, IdShortPath, Arg.Any<CancellationToken>())
470+
.Returns(fakeResult);
471+
472+
await _sut.GetFileAttachment(request, CancellationToken.None);
473+
474+
await _submodelRepository.Received(1)
475+
.GetFileAttachmentAsync(SubmodelId, IdShortPath, Arg.Any<CancellationToken>());
476+
}
477+
478+
[Fact]
479+
public async Task GetFileAttachment_ReturnsServiceResult_WhenServiceSucceeds()
480+
{
481+
const string SubmodelId = "NameplateSubmodel";
482+
const string IdShortPath = "Documents.ProductImage";
483+
var encodedId = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(SubmodelId));
484+
var request = new GetSubmodelElementRequest(encodedId, IdShortPath);
485+
486+
var fakeResult = new FileAttachmentResult(Stream.Null, "application/octet-stream", "file.bin", 100 * 1024 * 1024);
487+
488+
_submodelRepository
489+
.GetFileAttachmentAsync(SubmodelId, IdShortPath, Arg.Any<CancellationToken>())
490+
.Returns(fakeResult);
491+
492+
var result = await _sut.GetFileAttachment(request, CancellationToken.None);
493+
494+
Assert.Equal(fakeResult, result);
495+
}
496+
497+
[Fact]
498+
public async Task GetFileAttachment_InvalidBase64SubmodelId_ThrowsInvalidUserInputException()
499+
{
500+
const string InvalidEncodedId = "!!invalid_base64@@";
501+
const string IdShortPath = "Documents.ProductImage";
502+
var request = new GetSubmodelElementRequest(InvalidEncodedId, IdShortPath);
503+
504+
await Assert.ThrowsAsync<InvalidUserInputException>(() => _sut.GetFileAttachment(request, CancellationToken.None));
505+
}
506+
507+
[Theory]
508+
[InlineData("../../../etc/passwd")]
509+
[InlineData("..\\..\\..\\windows\\system32")]
510+
public async Task GetFileAttachment_PathTraversalInIdShortPath_ThrowsInvalidUserInputException(string maliciousPath)
511+
{
512+
const string SubmodelId = "NameplateSubmodel";
513+
var encodedId = SubmodelId.EncodeBase64Url();
514+
var request = new GetSubmodelElementRequest(encodedId, maliciousPath);
515+
516+
await Assert.ThrowsAsync<InvalidUserInputException>(() => _sut.GetFileAttachment(request, CancellationToken.None));
517+
}
455518
}

0 commit comments

Comments
 (0)