Skip to content

Commit 901c36d

Browse files
authored
Implement retrieval of All SubmodelElements (#177)
1 parent d96482b commit 901c36d

18 files changed

Lines changed: 802 additions & 19 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
meta {
2+
name: Get All Submodel Elements - Nameplate
3+
type: http
4+
seq: 1
5+
}
6+
7+
get {
8+
url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements
9+
body: none
10+
auth: inherit
11+
}
12+
13+
params:path {
14+
submodelIdentifier: {{submodelIdentifierNameplate-1}}
15+
}
16+
17+
params:query {
18+
~limit: 100
19+
~cursor:
20+
~level: deep
21+
~extent: withBlobValue
22+
}
23+
24+
settings {
25+
encodeUrl: true
26+
timeout: 0
27+
}
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using System.Text;
4+
using System.Text.Json.Nodes;
5+
6+
using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application;
7+
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers;
8+
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository;
9+
using AAS.TwinEngine.DataEngine.DomainModel.Shared;
10+
using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository;
11+
using AAS.TwinEngine.DataEngine.ModuleTests.Common;
12+
13+
using AasCore.Aas3_1;
14+
15+
using Microsoft.AspNetCore.WebUtilities;
16+
using Microsoft.Extensions.DependencyInjection;
17+
18+
using NSubstitute;
19+
using NSubstitute.ExceptionExtensions;
20+
21+
namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRepository;
22+
23+
public abstract class GetAllSubmodelElementsControllerTests : IDisposable
24+
{
25+
private readonly ConfigTestFactory _factory;
26+
private readonly ISubmodelRepositoryService _mockSubmodelRepositoryService;
27+
private readonly HttpClient _client;
28+
29+
private const string SubmodelId = "ContactInformation";
30+
31+
protected GetAllSubmodelElementsControllerTests(string configDir)
32+
{
33+
_mockSubmodelRepositoryService = Substitute.For<ISubmodelRepositoryService>();
34+
var mockPluginManifestProvider = Substitute.For<IPluginManifestProvider>();
35+
36+
_factory = new ConfigTestFactory(configDir, services =>
37+
{
38+
_ = services.AddSingleton(_mockSubmodelRepositoryService);
39+
_ = services.AddSingleton(mockPluginManifestProvider);
40+
});
41+
42+
_client = _factory.CreateClient();
43+
}
44+
45+
public void Dispose()
46+
{
47+
_client.Dispose();
48+
_factory.Dispose();
49+
GC.SuppressFinalize(this);
50+
}
51+
52+
private static string GetUrl(string? submodelId = null, int? limit = null, string? cursor = null)
53+
{
54+
var encodedId = submodelId is null
55+
? EncodeBase64Url(SubmodelId)
56+
: EncodeBase64Url(submodelId);
57+
var url = $"/submodels/{encodedId}/submodel-elements";
58+
var queryParams = new Dictionary<string, string?>();
59+
if (limit.HasValue)
60+
{
61+
queryParams["limit"] = limit.Value.ToString();
62+
}
63+
64+
if (cursor is not null)
65+
{
66+
queryParams["cursor"] = cursor;
67+
}
68+
69+
return queryParams.Count > 0 ? QueryHelpers.AddQueryString(url, queryParams) : url;
70+
}
71+
72+
[Fact]
73+
public async Task GetAllSubmodelElementsAsync_WithNoQueryParams_ReturnsOkWithEmptyResultAsync()
74+
{
75+
// Arrange
76+
var elementList = new SubmodelElementsPage
77+
{
78+
PagingMetaData = new PagingMetaData { Cursor = null },
79+
Result = []
80+
};
81+
82+
_ = _mockSubmodelRepositoryService
83+
.GetAllSubmodelElementsAsync(SubmodelId, null, null, null, Arg.Any<CancellationToken>())
84+
.Returns(elementList);
85+
86+
// Act
87+
var response = await _client.GetAsync(GetUrl());
88+
89+
// Assert
90+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
91+
var body = await response.Content.ReadFromJsonAsync<JsonObject>();
92+
Assert.NotNull(body);
93+
Assert.True(body.ContainsKey("result"));
94+
}
95+
96+
[Fact]
97+
public async Task GetAllSubmodelElementsAsync_WithElements_ReturnsPopulatedResultAsync()
98+
{
99+
// Arrange
100+
var element = TestData.CreateManufacturerName();
101+
var elementList = new SubmodelElementsPage
102+
{
103+
PagingMetaData = new PagingMetaData { Cursor = null },
104+
Result = [element]
105+
};
106+
107+
_ = _mockSubmodelRepositoryService
108+
.GetAllSubmodelElementsAsync(SubmodelId, null, null, null, Arg.Any<CancellationToken>())
109+
.Returns(elementList);
110+
111+
// Act
112+
var response = await _client.GetAsync(GetUrl());
113+
114+
// Assert
115+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
116+
var body = await response.Content.ReadFromJsonAsync<JsonObject>();
117+
Assert.NotNull(body);
118+
var resultArray = body["result"]?.AsArray();
119+
Assert.NotNull(resultArray);
120+
_ = Assert.Single(resultArray);
121+
}
122+
123+
[Fact]
124+
public async Task GetAllSubmodelElementsAsync_WithPagingCursorInResponse_ReturnsCursorInBodyAsync()
125+
{
126+
// Arrange
127+
const string ExpectedCursor = "dGVzdEN1cnNvcg==";
128+
var elementList = new SubmodelElementsPage
129+
{
130+
PagingMetaData = new PagingMetaData { Cursor = ExpectedCursor },
131+
Result = []
132+
};
133+
134+
_ = _mockSubmodelRepositoryService
135+
.GetAllSubmodelElementsAsync(SubmodelId, Arg.Any<SubmodelQueryOptions?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
136+
.Returns(elementList);
137+
138+
// Act
139+
var response = await _client.GetAsync(GetUrl(limit: 10));
140+
141+
// Assert
142+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
143+
var body = await response.Content.ReadFromJsonAsync<JsonObject>();
144+
Assert.NotNull(body);
145+
var pagingMetadata = body["paging_metadata"]?.AsObject();
146+
Assert.NotNull(pagingMetadata);
147+
Assert.Equal(ExpectedCursor, pagingMetadata["cursor"]?.GetValue<string>());
148+
}
149+
150+
[Fact]
151+
public async Task GetAllSubmodelElementsAsync_WithValidLimitAndCursor_ReturnsOkAsync()
152+
{
153+
// Arrange
154+
var cursor = EncodeBase64Url("next-page-token");
155+
var elementList = new SubmodelElementsPage
156+
{
157+
PagingMetaData = new PagingMetaData { Cursor = null },
158+
Result = []
159+
};
160+
161+
_ = _mockSubmodelRepositoryService
162+
.GetAllSubmodelElementsAsync(SubmodelId, Arg.Any<SubmodelQueryOptions?>(), 5, cursor, Arg.Any<CancellationToken>())
163+
.Returns(elementList);
164+
165+
// Act
166+
var response = await _client.GetAsync(GetUrl(limit: 5, cursor: cursor));
167+
168+
// Assert
169+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
170+
}
171+
172+
[Theory]
173+
[InlineData("deep")]
174+
[InlineData("core")]
175+
public async Task GetAllSubmodelElementsAsync_WithLevelQueryParam_ReturnsOkAsync(string level)
176+
{
177+
// Arrange
178+
var elementList = new SubmodelElementsPage
179+
{
180+
PagingMetaData = new PagingMetaData { Cursor = null },
181+
Result = []
182+
};
183+
184+
_ = _mockSubmodelRepositoryService
185+
.GetAllSubmodelElementsAsync(SubmodelId, Arg.Any<SubmodelQueryOptions?>(), null, null, Arg.Any<CancellationToken>())
186+
.Returns(elementList);
187+
188+
// Act
189+
var response = await _client.GetAsync(GetUrl() + $"?level={level}");
190+
191+
// Assert
192+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
193+
}
194+
195+
[Theory]
196+
[InlineData("withBlobValue")]
197+
[InlineData("withoutBlobValue")]
198+
public async Task GetAllSubmodelElementsAsync_WithExtentQueryParam_ReturnsOkAsync(string extent)
199+
{
200+
// Arrange
201+
var elementList = new SubmodelElementsPage
202+
{
203+
PagingMetaData = new PagingMetaData { Cursor = null },
204+
Result = []
205+
};
206+
207+
_ = _mockSubmodelRepositoryService
208+
.GetAllSubmodelElementsAsync(SubmodelId, Arg.Any<SubmodelQueryOptions?>(), null, null, Arg.Any<CancellationToken>())
209+
.Returns(elementList);
210+
211+
// Act
212+
var response = await _client.GetAsync(GetUrl() + $"?extent={extent}");
213+
214+
// Assert
215+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
216+
}
217+
218+
[Fact]
219+
public async Task GetAllSubmodelElementsAsync_ResponseBodyContainsResultAndPagingMetadata_Async()
220+
{
221+
// Arrange
222+
var elementList = new SubmodelElementsPage
223+
{
224+
PagingMetaData = new PagingMetaData { Cursor = null },
225+
Result = []
226+
};
227+
228+
_ = _mockSubmodelRepositoryService
229+
.GetAllSubmodelElementsAsync(Arg.Any<string>(), Arg.Any<SubmodelQueryOptions?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
230+
.Returns(elementList);
231+
232+
// Act
233+
var response = await _client.GetAsync(GetUrl());
234+
235+
// Assert
236+
var body = await response.Content.ReadFromJsonAsync<JsonObject>();
237+
Assert.NotNull(body);
238+
Assert.True(body.ContainsKey("result"), "Response body must contain a 'result' field.");
239+
Assert.True(body.ContainsKey("paging_metadata"), "Response body must contain a 'paging_metadata' field.");
240+
}
241+
242+
[Theory]
243+
[InlineData(0)]
244+
[InlineData(-1)]
245+
[InlineData(-100)]
246+
public async Task GetAllSubmodelElementsAsync_WithInvalidLimit_Returns400Async(int invalidLimit)
247+
{
248+
// Act
249+
var response = await _client.GetAsync(GetUrl(limit: invalidLimit));
250+
251+
// Assert
252+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
253+
}
254+
255+
[Fact]
256+
public async Task GetAllSubmodelElementsAsync_WithInvalidBase64SubmodelId_Returns400Async()
257+
{
258+
// Act
259+
var response = await _client.GetAsync("/submodels/not!!valid%%base64/submodel-elements");
260+
261+
// Assert
262+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
263+
}
264+
265+
[Fact]
266+
public async Task GetAllSubmodelElementsAsync_WhenSubmodelNotFound_Returns404Async()
267+
{
268+
// Arrange
269+
_ = _mockSubmodelRepositoryService
270+
.GetAllSubmodelElementsAsync(Arg.Any<string>(), Arg.Any<SubmodelQueryOptions?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
271+
.Throws(new SubmodelNotFoundException());
272+
273+
// Act
274+
var response = await _client.GetAsync(GetUrl());
275+
276+
// Assert
277+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
278+
}
279+
280+
[Fact]
281+
public async Task GetAllSubmodelElementsAsync_WhenServiceThrowsInternalDataProcessingException_Returns500Async()
282+
{
283+
// Arrange
284+
_ = _mockSubmodelRepositoryService
285+
.GetAllSubmodelElementsAsync(Arg.Any<string>(), Arg.Any<SubmodelQueryOptions?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
286+
.Throws(new InternalDataProcessingException());
287+
288+
// Act
289+
var response = await _client.GetAsync(GetUrl());
290+
291+
// Assert
292+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
293+
}
294+
295+
[Fact]
296+
public async Task GetAllSubmodelElementsAsync_WhenServiceThrowsUnexpectedException_Returns500Async()
297+
{
298+
// Arrange
299+
_ = _mockSubmodelRepositoryService
300+
.GetAllSubmodelElementsAsync(Arg.Any<string>(), Arg.Any<SubmodelQueryOptions?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
301+
.Throws(new InvalidOperationException("Unexpected failure"));
302+
303+
// Act
304+
var response = await _client.GetAsync(GetUrl());
305+
306+
// Assert
307+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
308+
}
309+
310+
private static string EncodeBase64Url(string plainText)
311+
{
312+
if (string.IsNullOrWhiteSpace(plainText))
313+
{
314+
return string.Empty;
315+
}
316+
317+
var bytes = Encoding.UTF8.GetBytes(plainText);
318+
return WebEncoders.Base64UrlEncode(bytes);
319+
}
320+
}
321+
322+
public class GetAllSubmodelElementsControllerTestsV1Config() : GetAllSubmodelElementsControllerTests("v1-config");
323+
324+
public class GetAllSubmodelElementsControllerTestsV2Config() : GetAllSubmodelElementsControllerTests("v2-config");

0 commit comments

Comments
 (0)