Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 5 additions & 4 deletions internal/domain/services/indexer_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,11 @@ func (s *IndexerService) decodeTransactionData(transaction *contracts.LFXTransac
// If there is no error, then we can unmarshal the data.
// Otherwise, it means the data wasn't base64 encoded and therefore
// we can just use the data as is.
switch {
case s.isCreateAction(transaction) || s.isUpdateAction(transaction):
logger.Debug("Transaction data is base64 encoded, decoding",
"transaction_id", transactionID,
"action", transaction.Action,
"object_type", transaction.ObjectType)
if s.isCreateAction(transaction) || s.isUpdateAction(transaction) {
var data map[string]any
if err := json.Unmarshal(decodedData, &data); err != nil {
logging.LogError(logger, "Failed to unmarshal JSON", err,
Expand All @@ -330,8 +333,6 @@ func (s *IndexerService) decodeTransactionData(transaction *contracts.LFXTransac
return err
}
transaction.Data = data
case s.isDeleteAction(transaction):
transaction.Data = string(decodedData)
}
}
}
Expand Down
126 changes: 126 additions & 0 deletions internal/domain/services/indexer_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package services

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"testing"
Expand All @@ -16,6 +17,7 @@ import (
"github.com/linuxfoundation/lfx-v2-indexer-service/pkg/logging"
"github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestIndexerService_ProcessTransaction_Success(t *testing.T) {
Expand Down Expand Up @@ -1318,3 +1320,127 @@ func TestIndexerService_ProcessTransaction_V1ActionCanonicalizedInEvent(t *testi
// Subject must use canonical past-tense "created", not raw "create"
assert.Equal(t, "lfx.project.created", mockMessagingRepo.PublishCalls[0].Subject)
}

func TestDecodeTransactionData(t *testing.T) {
mustBase64JSON := func(v map[string]any) string {
b, err := json.Marshal(v)
require.NoError(t, err)
return base64.StdEncoding.EncodeToString(b)
}

tests := []struct {
name string
transaction *contracts.LFXTransaction
wantErr bool
assertData func(t *testing.T, got any)
}{
// --- delete: ID must always be preserved as-is ---
{
name: "delete/uuid ID preserved - hyphen breaks base64",
transaction: &contracts.LFXTransaction{
Action: constants.ActionDeleted,
Data: "550e8400-e29b-41d4-a716-446655440000",
},
assertData: func(t *testing.T, got any) {
assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", got)
},
},
{
name: "delete/numeric ID preserved - length not multiple of 4 (regression: v1_past_meeting)",
transaction: &contracts.LFXTransaction{
Action: constants.ActionDeleted,
Data: "15554981610", // 11 chars — not a multiple of 4
},
assertData: func(t *testing.T, got any) {
assert.Equal(t, "15554981610", got)
},
},
{
name: "delete/alphanumeric ID length multiple of 4 preserved - was silently corrupted before fix",
transaction: &contracts.LFXTransaction{
Action: constants.ActionDeleted,
Data: "abcd", // 4 chars, valid base64 alphabet — triggered the bug
},
assertData: func(t *testing.T, got any) {
assert.Equal(t, "abcd", got)
},
},
{
name: "delete/v1 present-tense action preserved",
transaction: &contracts.LFXTransaction{
Action: constants.ActionDelete,
Data: "abcd", // valid base64, length 4
},
assertData: func(t *testing.T, got any) {
assert.Equal(t, "abcd", got)
},
},
// --- create/update: base64-encoded JSON must be decoded ---
{
name: "create/base64 JSON decoded and unmarshalled",
transaction: &contracts.LFXTransaction{
Action: constants.ActionCreated,
Data: mustBase64JSON(map[string]any{"id": "proj-1", "name": "Test"}),
},
assertData: func(t *testing.T, got any) {
data, ok := got.(map[string]any)
require.True(t, ok, "expected map[string]any")
assert.Equal(t, "proj-1", data["id"])
},
},
{
name: "update/base64 JSON decoded and unmarshalled",
transaction: &contracts.LFXTransaction{
Action: constants.ActionUpdated,
Data: mustBase64JSON(map[string]any{"id": "proj-2", "name": "Updated"}),
},
assertData: func(t *testing.T, got any) {
data, ok := got.(map[string]any)
require.True(t, ok, "expected map[string]any")
assert.Equal(t, "proj-2", data["id"])
},
},
{
name: "create/already a map - passed through untouched",
transaction: &contracts.LFXTransaction{
Action: constants.ActionCreated,
Data: map[string]any{"id": "proj-3"},
},
assertData: func(t *testing.T, got any) {
data, ok := got.(map[string]any)
require.True(t, ok, "expected map[string]any")
assert.Equal(t, "proj-3", data["id"])
},
},
{
name: "create/valid base64 but not JSON returns error",
transaction: &contracts.LFXTransaction{
Action: constants.ActionCreated,
Data: base64.StdEncoding.EncodeToString([]byte("this is not json")),
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, _ := logging.TestLogger(t)
s := NewIndexerService(
mocks.NewMockStorageRepository(),
mocks.NewMockMessagingRepository(),
logger,
)

err := s.decodeTransactionData(tt.transaction)

if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
if tt.assertData != nil {
tt.assertData(t, tt.transaction.Data)
}
})
}
}
40 changes: 39 additions & 1 deletion internal/enrichers/groupsio_mailing_list_enricher.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package enrichers

import (
"strings"

"github.com/linuxfoundation/lfx-v2-indexer-service/internal/domain/contracts"
"github.com/linuxfoundation/lfx-v2-indexer-service/pkg/constants"
)
Expand All @@ -24,9 +26,45 @@ func (e *GroupsIOMailingListEnricher) EnrichData(body *contracts.TransactionBody
return e.defaultEnricher.EnrichData(body, transaction)
}

// extractGroupName extracts the sort name for a mailing list, preferring group_name over generic name fields.
func extractGroupName(data map[string]any) string {
if v, ok := data["group_name"].(string); ok && v != "" {
return strings.TrimSpace(v)
}
return defaultExtractSortName(data)
}

// extractGroupNameAndAliases extracts name and aliases for a mailing list, including group_name.
func extractGroupNameAndAliases(data map[string]any) []string {
seen := make(map[string]bool)
var names []string

addUnique := func(s string) {
s = strings.TrimSpace(s)
if s != "" && !seen[s] {
names = append(names, s)
seen[s] = true
}
}

if v, ok := data["group_name"].(string); ok {
addUnique(v)
}

for _, alias := range defaultExtractNameAndAliases(data) {
addUnique(alias)
}

return names
}

// NewGroupsIOMailingListEnricher creates a new GroupsIO mailing list enricher
func NewGroupsIOMailingListEnricher() Enricher {
return &GroupsIOMailingListEnricher{
defaultEnricher: newDefaultEnricher(constants.ObjectTypeGroupsIOMailingList),
defaultEnricher: newDefaultEnricher(
constants.ObjectTypeGroupsIOMailingList,
WithSortName(extractGroupName),
WithNameAndAliases(extractGroupNameAndAliases),
),
}
}
134 changes: 134 additions & 0 deletions internal/enrichers/groupsio_mailing_list_enricher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

package enrichers

import (
"testing"

"github.com/linuxfoundation/lfx-v2-indexer-service/internal/domain/contracts"
"github.com/linuxfoundation/lfx-v2-indexer-service/pkg/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGroupsIOMailingListEnricher_ObjectType(t *testing.T) {
enricher := NewGroupsIOMailingListEnricher()
assert.Equal(t, constants.ObjectTypeGroupsIOMailingList, enricher.ObjectType())
}

func TestGroupsIOMailingListEnricher_EnrichData(t *testing.T) {
enricher := NewGroupsIOMailingListEnricher()

tests := []struct {
name string
parsedData map[string]any
expectedBody *contracts.TransactionBody
expectedError string
}{
{
name: "group_name used as sort name and alias",
parsedData: map[string]any{
"uid": "ml-123",
"group_name": "My Mailing List",
},
expectedBody: &contracts.TransactionBody{
ObjectID: "ml-123",
SortName: "My Mailing List",
NameAndAliases: []string{"My Mailing List"},
},
},
{
name: "group_name takes priority over name field",
parsedData: map[string]any{
"uid": "ml-456",
"group_name": "Group Name",
"name": "Other Name",
},
expectedBody: &contracts.TransactionBody{
ObjectID: "ml-456",
SortName: "Group Name",
},
},
{
name: "both group_name and name appear in aliases",
parsedData: map[string]any{
"uid": "ml-789",
"group_name": "Group Name",
"name": "Other Name",
},
expectedBody: &contracts.TransactionBody{
ObjectID: "ml-789",
SortName: "Group Name",
NameAndAliases: []string{"Group Name", "Other Name"},
},
},
{
name: "fallback to name when group_name absent",
parsedData: map[string]any{
"uid": "ml-fallback",
"name": "Fallback Name",
},
expectedBody: &contracts.TransactionBody{
ObjectID: "ml-fallback",
SortName: "Fallback Name",
NameAndAliases: []string{"Fallback Name"},
},
},
{
name: "group_name whitespace trimmed",
parsedData: map[string]any{
"uid": "ml-trim",
"group_name": " Trimmed ",
},
expectedBody: &contracts.TransactionBody{
ObjectID: "ml-trim",
SortName: "Trimmed",
NameAndAliases: []string{"Trimmed"},
},
},
{
name: "empty group_name falls back to name",
parsedData: map[string]any{
"uid": "ml-empty",
"group_name": "",
"name": "Fallback",
},
expectedBody: &contracts.TransactionBody{
ObjectID: "ml-empty",
SortName: "Fallback",
},
},
{
name: "error: missing uid and id",
parsedData: map[string]any{
"group_name": "Some List",
},
expectedError: constants.ErrMappingUID,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := &contracts.TransactionBody{}
transaction := &contracts.LFXTransaction{
ParsedData: tt.parsedData,
}

err := enricher.EnrichData(body, transaction)

if tt.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
return
}

require.NoError(t, err)
assert.Equal(t, tt.expectedBody.ObjectID, body.ObjectID)
assert.Equal(t, tt.expectedBody.SortName, body.SortName)
if tt.expectedBody.NameAndAliases != nil {
assert.Equal(t, tt.expectedBody.NameAndAliases, body.NameAndAliases)
}
})
}
}
Loading