From 7a375acfd791c67ffd1e0ab039e343c62afc1fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 5 Aug 2026 18:43:13 +0200 Subject: [PATCH] Fix missing favorite flag on opensearch hits --- .../internal/convert/convert_suite_test.go | 13 ++ .../opensearch/internal/convert/opensearch.go | 11 +- .../internal/convert/opensearch_test.go | 123 ++++++++++++++---- 3 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 services/search/pkg/opensearch/internal/convert/convert_suite_test.go diff --git a/services/search/pkg/opensearch/internal/convert/convert_suite_test.go b/services/search/pkg/opensearch/internal/convert/convert_suite_test.go new file mode 100644 index 0000000000..bcfc527452 --- /dev/null +++ b/services/search/pkg/opensearch/internal/convert/convert_suite_test.go @@ -0,0 +1,13 @@ +package convert_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConvert(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OpenSearch Convert Suite") +} diff --git a/services/search/pkg/opensearch/internal/convert/opensearch.go b/services/search/pkg/opensearch/internal/convert/opensearch.go index c4d8212dcd..16c156314f 100644 --- a/services/search/pkg/opensearch/internal/convert/opensearch.go +++ b/services/search/pkg/opensearch/internal/convert/opensearch.go @@ -55,11 +55,12 @@ func OpenSearchHitToMatch(hit opensearchgoAPI.SearchHit) (*searchMessage.Match, SpaceId: resourceParentID.GetSpaceId(), OpaqueId: resourceParentID.GetOpaqueId(), }, - Size: resource.Size, - Type: resource.Type, - MimeType: resource.MimeType, - Deleted: resource.Deleted, - Tags: resource.Tags, + Size: resource.Size, + Type: resource.Type, + MimeType: resource.MimeType, + Deleted: resource.Deleted, + Tags: resource.Tags, + Favorites: resource.Favorites, Highlights: func() string { contentHighlights, ok := hit.Highlight["Content"] if !ok { diff --git a/services/search/pkg/opensearch/internal/convert/opensearch_test.go b/services/search/pkg/opensearch/internal/convert/opensearch_test.go index afff90411e..bf80b7058f 100644 --- a/services/search/pkg/opensearch/internal/convert/opensearch_test.go +++ b/services/search/pkg/opensearch/internal/convert/opensearch_test.go @@ -2,37 +2,114 @@ package convert_test import ( "encoding/json" - "testing" + "time" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi" - "github.com/stretchr/testify/assert" "github.com/opencloud-eu/opencloud/pkg/conversions" searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/convert" - "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test" + opensearchtest "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test" + "github.com/opencloud-eu/opencloud/services/search/pkg/search" ) -func TestOpenSearchHitToMatch(t *testing.T) { - resource := opensearchtest.Testdata.Resources.File - resource.MimeType = "audio/anything" - - hit := opensearchgoAPI.SearchHit{ - Score: 1.1, - Source: json.RawMessage(opensearchtest.JSONMustMarshal(t, resource)), - } - match, err := convert.OpenSearchHitToMatch(hit) - assert.NoError(t, err) - assert.Equal(t, hit.Score, match.Score) - assert.Equal(t, resource.Name, match.Entity.Name) - t.Parallel() - t.Run("converts the audio field to the expected type", func(t *testing.T) { +// jsonMarshal marshals data to a JSON string, failing the running spec on error. +func jsonMarshal(data any) string { + GinkgoHelper() + b, err := json.Marshal(data) + Expect(err).ToNot(HaveOccurred()) + return string(b) +} + +var _ = Describe("OpenSearchHitToMatch", func() { + var ( + resource search.Resource + hit opensearchgoAPI.SearchHit + mtime time.Time + match *searchMessage.Match + err error + ) + + BeforeEach(func() { + resource = opensearchtest.Testdata.Resources.File + resource.MimeType = "audio/mpeg" + mtime = time.Date(2025, 7, 24, 15, 15, 1, 0, time.UTC) + resource.Mtime = mtime.Format(time.RFC3339) + resource.Favorites = []string{"cbf24bce-3e6e-4d9e-a2a2-cbf24bce3e6e"} + + hit = opensearchgoAPI.SearchHit{ + Score: 1.1, + Source: json.RawMessage(jsonMarshal(resource)), + Highlight: map[string][]string{ + "Content": {"first match", "second match"}, + }, + } + + match, err = convert.OpenSearchHitToMatch(hit) + Expect(err).ToNot(HaveOccurred()) + }) + + It("maps the score", func() { + Expect(match.Score).To(Equal(hit.Score)) + }) + + It("maps all resource fields to the entity", func() { + entity := match.Entity + Expect(entity).ToNot(BeNil()) + + // reference (derived from RootID) and path + Expect(entity.Ref.ResourceId.StorageId).To(Equal("1")) + Expect(entity.Ref.ResourceId.SpaceId).To(Equal("1")) + Expect(entity.Ref.ResourceId.OpaqueId).To(Equal("1")) + Expect(entity.Ref.Path).To(Equal(resource.Path)) + + // resource id + Expect(entity.Id.StorageId).To(Equal("1")) + Expect(entity.Id.SpaceId).To(Equal("1")) + Expect(entity.Id.OpaqueId).To(Equal("3")) + + // parent id + Expect(entity.ParentId.StorageId).To(Equal("1")) + Expect(entity.ParentId.SpaceId).To(Equal("1")) + Expect(entity.ParentId.OpaqueId).To(Equal("2")) + + // scalar fields + Expect(entity.Name).To(Equal(resource.Name)) + Expect(entity.Size).To(Equal(resource.Size)) + Expect(entity.Type).To(Equal(resource.Type)) + Expect(entity.MimeType).To(Equal(resource.MimeType)) + Expect(entity.Deleted).To(Equal(resource.Deleted)) + Expect(entity.Tags).To(Equal(resource.Tags)) + Expect(entity.Favorites).To(Equal(resource.Favorites)) + + // highlights are joined together + Expect(entity.Highlights).To(Equal("first match; second match")) + + // last modified time is parsed from the Mtime + Expect(entity.LastModifiedTime).ToNot(BeNil()) + Expect(entity.LastModifiedTime.Seconds).To(Equal(mtime.Unix())) + }) + + It("converts the media metadata to the expected types", func() { // searchMessage.Audio contains int64, int32 ... values that are converted to strings by the JSON marshaler, - // so we need to convert the resource.Audio to align the expectations for the JSON comparison. - audio, err := conversions.To[*searchMessage.Audio](resource.Audio) - assert.NoError(t, err) + // so we need to convert the resource fields to align the expectations for the JSON comparison. + expectedAudio, err := conversions.To[*searchMessage.Audio](resource.Audio) + Expect(err).ToNot(HaveOccurred()) + Expect(match.Entity.Audio.Bitrate).To(Equal(resource.Audio.Bitrate)) + Expect(jsonMarshal(match.Entity.Audio)).To(MatchJSON(jsonMarshal(expectedAudio))) + + expectedImage, err := conversions.To[*searchMessage.Image](resource.Image) + Expect(err).ToNot(HaveOccurred()) + Expect(jsonMarshal(match.Entity.Image)).To(MatchJSON(jsonMarshal(expectedImage))) - assert.Equal(t, resource.Audio.Bitrate, match.Entity.Audio.Bitrate) - assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, audio), opensearchtest.JSONMustMarshal(t, match.Entity.Audio)) + expectedLocation, err := conversions.To[*searchMessage.GeoCoordinates](resource.Location) + Expect(err).ToNot(HaveOccurred()) + Expect(jsonMarshal(match.Entity.Location)).To(MatchJSON(jsonMarshal(expectedLocation))) + + expectedPhoto, err := conversions.To[*searchMessage.Photo](resource.Photo) + Expect(err).ToNot(HaveOccurred()) + Expect(jsonMarshal(match.Entity.Photo)).To(MatchJSON(jsonMarshal(expectedPhoto))) }) -} +})