Skip to content
Closed
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ sequenceDiagram
C-->>B: Submodel Template (Semantic IDs)
B->>B: Extract semantic IDs
B->>D: POST /data/{submodelId}
D-->D: Resolve semantic IDs
D-->>D: Resolve semantic IDs
D-->>B: Semantic Id with Values
B->>B: Populate Template
B-->>A: Filled submodel
Expand All @@ -68,7 +68,7 @@ The DataEngine transforms **static AAS templates** into **live digital represent

When a client requests AAS data (shell descriptor, submodel, or submodel element):

1. **Fetch Template** - DataEngine retrieves the required AAS/Submodel template from the AAS Template Registry
1. **Fetch Template** - DataEngine retrieves the required AAS/Submodel template from the Template Registry/Template Repository
2. **Extract Semantic IDs** - Identifies all semantic IDs within the template that need values
3. **Request Data via Schema** - Sends a JSON Schema to the Plugin API describing the structure and semantic IDs needed
4. **Receive Values** - Plugin queries its database and responds with populated values for the requested semantic IDs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Net;
using System.Net;

using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure;
using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config;
Expand All @@ -7,6 +7,8 @@
using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services;
using AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services;

using AasCore.Aas3_1;

using Microsoft.Extensions.Logging;

using NSubstitute;
Expand Down Expand Up @@ -44,6 +46,81 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ReturnsSubmodelDesciptor
Assert.Equal(expectedDescriptor.Id, result.Id);
}

[Fact]
public async Task GetDataForSubmodelDescriptorByIdAsync_DeserializesAasNestedTypes_WhenResponseContainsAasPayload()
{
const string id = "https://mm-software.com/submodel/nameplate";
const string jsonResponse = """
{
"description": [
{
"language": "en",
"text": "Nameplate Submodel Template"
}
],
"displayName": [
{
"language": "en",
"text": "Nameplate"
}
],
"extensions": [
{
"name": "templateSource",
"valueType": "xs:string",
"value": "Nameplate"
}
],
"administration": {
"version": "1",
"revision": "0"
},
"idShort": "Nameplate",
"id": "https://mm-software.com/submodel/nameplate",
"semanticId": {
"type": "ExternalReference",
"keys": [
{
"type": "GlobalReference",
"value": "https://admin-shell.io/zvei/nameplate/2/0/Nameplate"
}
]
}
}
""";

using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(jsonResponse)
}));
using var httpClient = new HttpClient(messageHandler);
httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl");
_clientFactory.CreateClient(HttpClientNames.SubmodelRegistry).Returns(httpClient);

var result = await _sut.GetDataForSubmodelDescriptorByIdAsync(id, CancellationToken.None);

Assert.NotNull(result.Description);
Assert.Equal("en", result.Description![0].Language);
Assert.Equal("Nameplate Submodel Template", result.Description[0].Text);

Assert.NotNull(result.DisplayName);
Assert.Equal("Nameplate", result.DisplayName![0].Text);

Assert.NotNull(result.Extensions);
Assert.Equal("templateSource", result.Extensions![0].Name);
Assert.Equal(DataTypeDefXsd.String, result.Extensions[0].ValueType);
Assert.Equal("Nameplate", result.Extensions[0].Value);

Assert.NotNull(result.Administration);
Assert.Equal("1", result.Administration!.Version);
Assert.Equal("0", result.Administration.Revision);

Assert.NotNull(result.SemanticId);
Assert.Equal(ReferenceTypes.ExternalReference, result.SemanticId!.Type);
Assert.Equal("https://admin-shell.io/zvei/nameplate/2/0/Nameplate", result.SemanticId.Keys[0].Value);
}

[Fact]
public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResponseParsingException_WhenDeserializationFails()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,85 @@ public async Task GetShellDescriptorTemplateAsync_ReturnsShellDescriptor_WhenVal
Assert.Equal("https://admin-shell.io/idta/asset/ContactInformation/1/0", result.GlobalAssetId);
}

[Fact]
public async Task GetShellDescriptorTemplateAsync_DeserializesAasNestedTypes_WhenResponseContainsAasPayload()
{
const string JsonResponse = """
{
"description": [
{
"language": "en",
"text": "Template Asset Administration Shell for example environments."
}
],
"displayName": [
{
"language": "en",
"text": "AAS Template"
}
],
"extensions": [
{
"name": "templateSource",
"valueType": "xs:string",
"value": "ShellTemplate"
}
],
"administration": {
"version": "1",
"revision": "0"
},
"assetKind": "Instance",
"id": "https://mm-software.com/aas/aasTemplate",
"specificAssetIds": [
{
"name": "SerialNumber",
"value": "SN-9429",
"externalSubjectId": {
"type": "ExternalReference",
"keys": [
{
"type": "GlobalReference",
"value": "https://example.com/subjects/serial-number"
}
]
}
}
]
}
""";

using var mockHttpResponse = new HttpResponseMessage(HttpStatusCode.OK);
mockHttpResponse.Content = new StringContent(JsonResponse);
using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse);
using var httpClient = new HttpClient(mockHttpMessageHandler);
httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl");
_httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient);

var result = await _sut.GetShellDescriptorTemplateAsync(TemplateId, CancellationToken.None);

Assert.NotNull(result.Description);
Assert.Equal("en", result.Description![0].Language);
Assert.Equal("Template Asset Administration Shell for example environments.", result.Description[0].Text);

Assert.NotNull(result.DisplayName);
Assert.Equal("AAS Template", result.DisplayName![0].Text);

Assert.NotNull(result.Extensions);
Assert.Equal("templateSource", result.Extensions![0].Name);
Assert.Equal(DataTypeDefXsd.String, result.Extensions[0].ValueType);
Assert.Equal("ShellTemplate", result.Extensions[0].Value);

Assert.NotNull(result.Administration);
Assert.Equal("1", result.Administration!.Version);
Assert.Equal("0", result.Administration.Revision);

Assert.NotNull(result.SpecificAssetIds);
Assert.Equal("SerialNumber", result.SpecificAssetIds![0].Name);
Assert.NotNull(result.SpecificAssetIds[0].ExternalSubjectId);
Assert.Equal("https://example.com/subjects/serial-number", result.SpecificAssetIds[0].ExternalSubjectId!.Keys[0].Value);
}

[Fact]
public async Task GetShellDescriptorTemplateAsync_ThrowsResponseParsingException_WhenInvalidJsonResponse()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System.Text.Json;
using System.Text.Json.Nodes;

using AAS.TwinEngine.DataEngine.Infrastructure.Shared;

using AasCore.Aas3_1;

namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Shared;

public class AasJsonNodeDeserializerTests
{
[Fact]
public void UnwrapResult_ReturnsResultNode_WhenResultPropertyExists()
{
var root = JsonNode.Parse("""
{
"result": {
"id": "shell-1"
}
}
""");

var result = AasJsonNodeDeserializer.UnwrapResult(root);

Assert.NotNull(result);
Assert.Equal("shell-1", result!["id"]!.GetValue<string>());
}

[Fact]
public void UnwrapResult_ReturnsRootNode_WhenResultPropertyDoesNotExist()
{
var root = JsonNode.Parse("""
{
"id": "shell-2"
}
""");

var result = AasJsonNodeDeserializer.UnwrapResult(root);

Assert.Same(root, result);
}

[Fact]
public void DeserializeAasNode_ReturnsNull_WhenNodeIsNull()
{
var result = AasJsonNodeDeserializer.DeserializeAasNode<string>(null, _ => "value");

Assert.Null(result);
}

[Fact]
public void DeserializeAasNode_ReturnsDeserializedValue_WhenNodeIsProvided()
{
var node = JsonNode.Parse("""{ "value": "abc" }""");

var result = AasJsonNodeDeserializer.DeserializeAasNode(node, n => n["value"]?.GetValue<string>());

Assert.Equal("abc", result);
}

[Fact]
public void DeserializeAasArray_ReturnsNull_WhenNodeIsNotArray()
{
var node = JsonNode.Parse("""{ "name": "not-an-array" }""");

var result = AasJsonNodeDeserializer.DeserializeAasArray(node, n => n["name"]?.GetValue<string>());

Assert.Null(result);
}

[Fact]
public void DeserializeAasArray_ReturnsFilteredArray_WhenArrayContainsNullEntries()
{
var node = JsonNode.Parse("""
[
{ "name": "one" },
null,
{ "name": "two" }
]
""");

var result = AasJsonNodeDeserializer.DeserializeAasArray(node, n => n["name"]?.GetValue<string>());

Assert.NotNull(result);
Assert.Equal(2, result!.Count);
Assert.Equal("one", result[0]);
Assert.Equal("two", result[1]);
}

[Fact]
public void DeserializeObject_ReturnsDefault_WhenNodeIsNull()
{
var result = AasJsonNodeDeserializer.DeserializeObject<TestDto>(null);

Assert.Null(result);
}

[Fact]
public void DeserializeObject_UsesDefaultOptions_WhenOptionsAreNotProvided()
{
var node = JsonNode.Parse("""{ "name": "Default" }""");

var result = AasJsonNodeDeserializer.DeserializeObject<TestDto>(node);

Assert.NotNull(result);
Assert.Equal("Default", result!.Name);
}

[Fact]
public void DeserializeObject_UsesProvidedOptions_WhenOptionsAreProvided()
{
var node = JsonNode.Parse("""{ "NAME": "CaseInsensitive" }""");
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

var result = AasJsonNodeDeserializer.DeserializeObject<TestDto>(node, options);

Assert.NotNull(result);
Assert.Equal("CaseInsensitive", result!.Name);
}

[Fact]
public void DeserializeEnum_ReturnsNull_WhenNodeIsNull()
{
var result = AasJsonNodeDeserializer.DeserializeEnum<AssetKind>(null);

Assert.Null(result);
}

[Fact]
public void DeserializeEnum_ReturnsEnumValue_WhenNodeContainsValidValue()
{
var node = JsonNode.Parse("\"Instance\"");

var result = AasJsonNodeDeserializer.DeserializeEnum<AssetKind>(node);

Assert.Equal(AssetKind.Instance, result);
}

[Fact]
public void DeserializeEnum_ReturnsNull_WhenNodeContainsInvalidValue()
{
var node = JsonNode.Parse("\"NotARealEnumValue\"");

var result = AasJsonNodeDeserializer.DeserializeEnum<AssetKind>(node);

Assert.Null(result);
}

private sealed class TestDto
{
public string? Name { get; init; }
}
}
Loading
Loading