Skip to content

Commit 1205f21

Browse files
authored
Fixes problems with aas repo thumbnails (#214)
* Fixes problems with aas repo thumbnails * Improves thumbnail tests
1 parent 210c8e4 commit 1205f21

File tree

3 files changed

+218
-4
lines changed

3 files changed

+218
-4
lines changed

internal/aasrepository/integration_tests/aasrepository_integration_test.go

Lines changed: 185 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ package main
2828

2929
import (
3030
"bytes"
31+
"database/sql"
3132
"encoding/base64"
3233
"encoding/json"
3334
"fmt"
@@ -40,13 +41,25 @@ import (
4041
"testing"
4142
"time"
4243

44+
"github.com/doug-martin/goqu/v9"
4345
"github.com/eclipse-basyx/basyx-go-components/internal/common/testenv"
4446
_ "github.com/lib/pq" // PostgreSQL Treiber
4547
"github.com/stretchr/testify/assert"
4648
"github.com/stretchr/testify/require"
4749
)
4850

4951
const actionDeleteAllAAS = "DELETE_ALL_AAS"
52+
const defaultIntegrationTestDSN = "postgres://admin:admin123@127.0.0.1:6432/basyxTestDB?sslmode=disable"
53+
54+
var integrationTestDSN = getIntegrationTestDSN()
55+
56+
func getIntegrationTestDSN() string {
57+
if dsn := os.Getenv("AASREPOSITORY_INTEGRATION_TEST_DSN"); dsn != "" {
58+
return dsn
59+
}
60+
61+
return defaultIntegrationTestDSN
62+
}
5063

5164
func deleteAllAAS(t *testing.T, runner *testenv.JSONSuiteRunner, stepNumber int) {
5265
for {
@@ -165,6 +178,103 @@ func downloadThumbnail(endpoint string) ([]byte, string, int, error) {
165178
return body, resp.Header.Get("Content-Type"), resp.StatusCode, nil
166179
}
167180

181+
func getJSONResponse(endpoint string) (map[string]any, int, error) {
182+
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
183+
if err != nil {
184+
return nil, 0, fmt.Errorf("failed to create request: %v", err)
185+
}
186+
187+
client := &http.Client{Timeout: 10 * time.Second}
188+
resp, err := client.Do(req)
189+
if err != nil {
190+
return nil, 0, fmt.Errorf("failed to send request: %v", err)
191+
}
192+
defer func() { _ = resp.Body.Close() }()
193+
194+
body, err := io.ReadAll(resp.Body)
195+
if err != nil {
196+
return nil, resp.StatusCode, fmt.Errorf("failed to read response: %v", err)
197+
}
198+
199+
var payload map[string]any
200+
if err = json.Unmarshal(body, &payload); err != nil {
201+
return nil, resp.StatusCode, fmt.Errorf("failed to unmarshal response: %v", err)
202+
}
203+
204+
return payload, resp.StatusCode, nil
205+
}
206+
207+
func getThumbnailWithoutFollowingRedirect(endpoint string) (int, string, error) {
208+
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
209+
if err != nil {
210+
return 0, "", fmt.Errorf("failed to create request: %v", err)
211+
}
212+
213+
client := &http.Client{
214+
Timeout: 10 * time.Second,
215+
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
216+
return http.ErrUseLastResponse
217+
},
218+
}
219+
220+
resp, err := client.Do(req)
221+
if err != nil {
222+
return 0, "", fmt.Errorf("failed to send request: %v", err)
223+
}
224+
defer func() { _ = resp.Body.Close() }()
225+
226+
return resp.StatusCode, resp.Header.Get("Location"), nil
227+
}
228+
229+
func setExternalThumbnailForAAS(aasID string, externalURL string) error {
230+
db, err := sql.Open("postgres", integrationTestDSN)
231+
if err != nil {
232+
return fmt.Errorf("failed to open db connection: %v", err)
233+
}
234+
defer func() { _ = db.Close() }()
235+
236+
dialect := goqu.Dialect("postgres")
237+
238+
selectAASDBIDSQL, selectAASDBIDArgs, selectAASDBIDBuildErr := dialect.
239+
From(goqu.T("aas")).
240+
Select(goqu.I("id")).
241+
Where(goqu.I("aas_id").Eq(aasID)).
242+
Limit(1).
243+
ToSQL()
244+
if selectAASDBIDBuildErr != nil {
245+
return fmt.Errorf("failed to build aas id query: %v", selectAASDBIDBuildErr)
246+
}
247+
248+
var aasDBID int64
249+
if queryErr := db.QueryRow(selectAASDBIDSQL, selectAASDBIDArgs...).Scan(&aasDBID); queryErr != nil {
250+
return fmt.Errorf("failed to query aas db id: %v", queryErr)
251+
}
252+
253+
upsertThumbnailSQL, upsertThumbnailArgs, upsertThumbnailBuildErr := dialect.
254+
Insert(goqu.T("thumbnail_file_element")).
255+
Rows(goqu.Record{
256+
"id": aasDBID,
257+
"content_type": "application/octet-stream",
258+
"file_name": "external-thumbnail",
259+
"value": externalURL,
260+
}).
261+
OnConflict(goqu.DoUpdate("id", goqu.Record{
262+
"content_type": "application/octet-stream",
263+
"file_name": "external-thumbnail",
264+
"value": externalURL,
265+
})).
266+
ToSQL()
267+
if upsertThumbnailBuildErr != nil {
268+
return fmt.Errorf("failed to build thumbnail upsert query: %v", upsertThumbnailBuildErr)
269+
}
270+
271+
if _, execErr := db.Exec(upsertThumbnailSQL, upsertThumbnailArgs...); execErr != nil {
272+
return fmt.Errorf("failed to upsert thumbnail element: %v", execErr)
273+
}
274+
275+
return nil
276+
}
277+
168278
// IntegrationTest runs the integration tests based on the config file
169279
func TestIntegration(t *testing.T) {
170280
testenv.RunJSONSuite(t, testenv.JSONSuiteOptions{
@@ -175,7 +285,7 @@ func TestIntegration(t *testing.T) {
175285
},
176286
testenv.ActionAssertSubmodelAbsent: testenv.NewCheckSubmodelAbsentAction(testenv.CheckSubmodelAbsentOptions{
177287
Driver: "postgres",
178-
DSN: "postgres://admin:admin123@127.0.0.1:6432/basyxTestDB?sslmode=disable",
288+
DSN: integrationTestDSN,
179289
}),
180290
},
181291
StepName: func(step testenv.JSONSuiteStep, stepNumber int) string {
@@ -219,7 +329,78 @@ func TestThumbnailAttachmentOperations(t *testing.T) {
219329
t.Logf("Thumbnail content verified: %d bytes", len(content))
220330
})
221331

222-
t.Run("3_Delete_Thumbnail", func(t *testing.T) {
332+
t.Run("3_Get_AAS_By_ID_Includes_Thumbnail_In_AssetInformation", func(t *testing.T) {
333+
aasEndpoint := fmt.Sprintf("%s/shells/%s", baseURL, aasIdentifier)
334+
payload, getStatus, getErr := getJSONResponse(aasEndpoint)
335+
require.NoError(t, getErr, "AAS retrieval failed")
336+
assert.Equal(t, http.StatusOK, getStatus, "Expected 200 OK for AAS retrieval")
337+
338+
assetInformation, ok := payload["assetInformation"].(map[string]any)
339+
require.True(t, ok, "assetInformation should be present")
340+
341+
thumbnail, ok := assetInformation["thumbnail"].(map[string]any)
342+
require.True(t, ok, "assetInformation.thumbnail should be present")
343+
344+
thumbnailPath, ok := thumbnail["path"].(string)
345+
require.True(t, ok, "thumbnail.path should be a string")
346+
assert.NotEmpty(t, thumbnailPath, "thumbnail.path should not be empty")
347+
348+
thumbnailContentType, ok := thumbnail["contentType"].(string)
349+
require.True(t, ok, "thumbnail.contentType should be a string")
350+
assert.Equal(t, "image/gif", thumbnailContentType, "thumbnail.contentType should match uploaded file")
351+
})
352+
353+
t.Run("4_Get_AAS_List_Includes_Thumbnail_In_AssetInformation", func(t *testing.T) {
354+
listEndpoint := fmt.Sprintf("%s/shells", baseURL)
355+
payload, getStatus, getErr := getJSONResponse(listEndpoint)
356+
require.NoError(t, getErr, "AAS list retrieval failed")
357+
assert.Equal(t, http.StatusOK, getStatus, "Expected 200 OK for AAS list retrieval")
358+
359+
result, ok := payload["result"].([]any)
360+
require.True(t, ok, "result should be an array")
361+
362+
foundAAS := false
363+
for _, entry := range result {
364+
aasMap, ok := entry.(map[string]any)
365+
if !ok {
366+
continue
367+
}
368+
if aasMap["id"] != aasID {
369+
continue
370+
}
371+
372+
foundAAS = true
373+
assetInformation, ok := aasMap["assetInformation"].(map[string]any)
374+
require.True(t, ok, "assetInformation should be present in listed AAS")
375+
376+
thumbnail, ok := assetInformation["thumbnail"].(map[string]any)
377+
require.True(t, ok, "assetInformation.thumbnail should be present in listed AAS")
378+
379+
thumbnailPath, ok := thumbnail["path"].(string)
380+
require.True(t, ok, "thumbnail.path should be a string in listed AAS")
381+
assert.NotEmpty(t, thumbnailPath, "thumbnail.path should not be empty in listed AAS")
382+
383+
thumbnailContentType, ok := thumbnail["contentType"].(string)
384+
require.True(t, ok, "thumbnail.contentType should be a string in listed AAS")
385+
assert.Equal(t, "image/gif", thumbnailContentType, "thumbnail.contentType should match uploaded file in listed AAS")
386+
break
387+
}
388+
389+
assert.True(t, foundAAS, "Expected uploaded AAS to be present in list response")
390+
})
391+
392+
t.Run("5_Get_Thumbnail_Redirects_For_External_URL", func(t *testing.T) {
393+
externalThumbnailURL := "https://example.com/assets/thumbs/thumbnail-external.gif"
394+
setErr := setExternalThumbnailForAAS(aasID, externalThumbnailURL)
395+
require.NoError(t, setErr, "Failed to set external thumbnail URL")
396+
397+
statusCode, locationHeader, requestErr := getThumbnailWithoutFollowingRedirect(thumbnailEndpoint)
398+
require.NoError(t, requestErr, "GET thumbnail request failed")
399+
assert.Equal(t, http.StatusFound, statusCode, "Expected 302 Found for external thumbnail URL")
400+
assert.Equal(t, externalThumbnailURL, locationHeader, "Expected redirect Location header for external thumbnail URL")
401+
})
402+
403+
t.Run("6_Delete_Thumbnail", func(t *testing.T) {
223404
req, reqErr := http.NewRequest(http.MethodDelete, thumbnailEndpoint, nil)
224405
require.NoError(t, reqErr, "Failed to create DELETE request")
225406

@@ -231,13 +412,13 @@ func TestThumbnailAttachmentOperations(t *testing.T) {
231412
assert.Equal(t, http.StatusNoContent, resp.StatusCode, "Expected 204 No Content for thumbnail deletion")
232413
})
233414

234-
t.Run("4_Verify_Thumbnail_Deleted", func(t *testing.T) {
415+
t.Run("7_Verify_Thumbnail_Deleted", func(t *testing.T) {
235416
_, _, getStatus, getErr := downloadThumbnail(thumbnailEndpoint)
236417
require.NoError(t, getErr, "Thumbnail download after delete should not fail at HTTP level")
237418
assert.Equal(t, http.StatusNotFound, getStatus, "Expected 404 Not Found after thumbnail deletion")
238419
})
239420

240-
t.Run("5_Upload_Thumbnail_For_NonExisting_AAS", func(t *testing.T) {
421+
t.Run("8_Upload_Thumbnail_For_NonExisting_AAS", func(t *testing.T) {
241422
nonExistingID := base64.RawStdEncoding.EncodeToString([]byte("https://example.com/ids/aas/non_existing_thumbnail_test"))
242423
nonExistingEndpoint := fmt.Sprintf("%s/shells/%s/asset-information/thumbnail", baseURL, nonExistingID)
243424

internal/aasrepository/persistence/aas_database.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,8 @@ func (s *AssetAdministrationShellDatabase) getAssetAdministrationShellMapByDBID(
899899
var assetKind sql.NullInt64
900900
var globalAssetID sql.NullString
901901
var assetType sql.NullString
902+
var thumbnailPath sql.NullString
903+
var thumbnailContentType sql.NullString
902904

903905
if queryErr := s.db.QueryRow(querySQL, queryArgs...).Scan(
904906
&aasID,
@@ -913,6 +915,8 @@ func (s *AssetAdministrationShellDatabase) getAssetAdministrationShellMapByDBID(
913915
&assetKind,
914916
&globalAssetID,
915917
&assetType,
918+
&thumbnailPath,
919+
&thumbnailContentType,
916920
); queryErr != nil {
917921
if queryErr == sql.ErrNoRows {
918922
return nil, common.NewErrNotFound("AASREPO-MAPAAS-AASNOTFOUND Asset Administration Shell not found")
@@ -963,6 +967,9 @@ func (s *AssetAdministrationShellDatabase) getAssetAdministrationShellMapByDBID(
963967
if assetType.Valid && assetType.String != "" {
964968
assetInfo["assetType"] = assetType.String
965969
}
970+
if thumbnailMap := buildThumbnailMap(thumbnailPath, thumbnailContentType); len(thumbnailMap) > 0 {
971+
assetInfo["thumbnail"] = thumbnailMap
972+
}
966973

967974
specificAssetIDs, specificErr := s.readSpecificAssetIDsByAssetInformationID(ctx, aasDBID)
968975
if specificErr != nil {
@@ -1044,6 +1051,8 @@ func (s *AssetAdministrationShellDatabase) getAssetAdministrationShellMapsByDBID
10441051
assetKind sql.NullInt64
10451052
globalAssetID sql.NullString
10461053
assetType sql.NullString
1054+
thumbnailPath sql.NullString
1055+
thumbnailContentType sql.NullString
10471056
}
10481057

10491058
rows, queryErr := s.db.QueryContext(ctx, querySQL, queryArgs...)
@@ -1072,6 +1081,8 @@ func (s *AssetAdministrationShellDatabase) getAssetAdministrationShellMapsByDBID
10721081
&row.assetKind,
10731082
&row.globalAssetID,
10741083
&row.assetType,
1084+
&row.thumbnailPath,
1085+
&row.thumbnailContentType,
10751086
); scanErr != nil {
10761087
return nil, common.NewInternalServerError("AASREPO-MAPAASBATCH-SCANROW " + scanErr.Error())
10771088
}
@@ -1142,6 +1153,9 @@ func (s *AssetAdministrationShellDatabase) getAssetAdministrationShellMapsByDBID
11421153
if row.assetType.Valid && row.assetType.String != "" {
11431154
assetInfo["assetType"] = row.assetType.String
11441155
}
1156+
if thumbnailMap := buildThumbnailMap(row.thumbnailPath, row.thumbnailContentType); len(thumbnailMap) > 0 {
1157+
assetInfo["thumbnail"] = thumbnailMap
1158+
}
11451159

11461160
specificAssetIDs := specificAssetIDsByAASID[aasDBID]
11471161
if len(specificAssetIDs) > 0 {
@@ -1227,6 +1241,19 @@ func assignJSONPayload(target map[string]any, key string, payload []byte) error
12271241
return nil
12281242
}
12291243

1244+
func buildThumbnailMap(path sql.NullString, contentType sql.NullString) map[string]any {
1245+
if !path.Valid || path.String == "" {
1246+
return nil
1247+
}
1248+
1249+
thumbnail := map[string]any{"path": path.String}
1250+
if contentType.Valid && contentType.String != "" {
1251+
thumbnail["contentType"] = contentType.String
1252+
}
1253+
1254+
return thumbnail
1255+
}
1256+
12301257
// parseSpecificAssetIDSemanticIDPayload parses an optional SpecificAssetID
12311258
// semanticId payload and reports whether parsing produced a semanticId.
12321259
func parseSpecificAssetIDSemanticIDPayload(payload []byte) (types.IReference, bool, error) {

internal/aasrepository/persistence/aas_database_query_utils.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ func buildGetAssetAdministrationShellMapByDBIDQuery(dialect *goqu.DialectWrapper
249249
From(goqu.T("aas").As("a")).
250250
LeftJoin(goqu.T("aas_payload").As("ap"), goqu.On(goqu.I("ap.aas_id").Eq(goqu.I("a.id")))).
251251
LeftJoin(goqu.T("asset_information").As("ai"), goqu.On(goqu.I("ai.asset_information_id").Eq(goqu.I("a.id")))).
252+
LeftJoin(goqu.T("thumbnail_file_element").As("tfe"), goqu.On(goqu.I("tfe.id").Eq(goqu.I("a.id")))).
252253
Select(
253254
goqu.I("a.aas_id"),
254255
goqu.I("a.id_short"),
@@ -262,6 +263,8 @@ func buildGetAssetAdministrationShellMapByDBIDQuery(dialect *goqu.DialectWrapper
262263
goqu.I("ai.asset_kind"),
263264
goqu.I("ai.global_asset_id"),
264265
goqu.I("ai.asset_type"),
266+
goqu.I("tfe.value"),
267+
goqu.I("tfe.content_type"),
265268
).
266269
Where(goqu.I("a.id").Eq(aasDBID)).
267270
ToSQL()
@@ -272,6 +275,7 @@ func buildGetAssetAdministrationShellMapsByDBIDsQuery(dialect *goqu.DialectWrapp
272275
From(goqu.T("aas").As("a")).
273276
LeftJoin(goqu.T("aas_payload").As("ap"), goqu.On(goqu.I("ap.aas_id").Eq(goqu.I("a.id")))).
274277
LeftJoin(goqu.T("asset_information").As("ai"), goqu.On(goqu.I("ai.asset_information_id").Eq(goqu.I("a.id")))).
278+
LeftJoin(goqu.T("thumbnail_file_element").As("tfe"), goqu.On(goqu.I("tfe.id").Eq(goqu.I("a.id")))).
275279
Select(
276280
goqu.I("a.id"),
277281
goqu.I("a.aas_id"),
@@ -286,6 +290,8 @@ func buildGetAssetAdministrationShellMapsByDBIDsQuery(dialect *goqu.DialectWrapp
286290
goqu.I("ai.asset_kind"),
287291
goqu.I("ai.global_asset_id"),
288292
goqu.I("ai.asset_type"),
293+
goqu.I("tfe.value"),
294+
goqu.I("tfe.content_type"),
289295
).
290296
Where(goqu.I("a.id").In(aasDBIDs)).
291297
ToSQL()

0 commit comments

Comments
 (0)