From af3deb0b30a5fe8c0c964015ef2681ab7ea9365a Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Fri, 2 Jun 2023 22:35:50 +0200
Subject: [PATCH] GitLab migration: Sanitize response for reaction list
 (#25054)

---
 services/migrations/gitlab.go      | 35 ++++++++++++----------
 services/migrations/gitlab_test.go | 47 ++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+), 15 deletions(-)

diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 015c38cd3b..76180a5159 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -413,7 +413,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
 			milestone = issue.Milestone.Title
 		}
 
-		var reactions []*base.Reaction
+		var reactions []*gitlab.AwardEmoji
 		awardPage := 1
 		for {
 			awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
@@ -421,9 +421,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
 				return nil, false, fmt.Errorf("error while listing issue awards: %w", err)
 			}
 
-			for i := range awards {
-				reactions = append(reactions, g.awardToReaction(awards[i]))
-			}
+			reactions = append(reactions, awards...)
 
 			if len(awards) < perPage {
 				break
@@ -442,7 +440,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
 			State:        issue.State,
 			Created:      *issue.CreatedAt,
 			Labels:       labels,
-			Reactions:    reactions,
+			Reactions:    g.awardsToReactions(reactions),
 			Closed:       issue.ClosedAt,
 			IsLocked:     issue.DiscussionLocked,
 			Updated:      *issue.UpdatedAt,
@@ -577,7 +575,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
 			milestone = pr.Milestone.Title
 		}
 
-		var reactions []*base.Reaction
+		var reactions []*gitlab.AwardEmoji
 		awardPage := 1
 		for {
 			awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
@@ -585,9 +583,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
 				return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err)
 			}
 
-			for i := range awards {
-				reactions = append(reactions, g.awardToReaction(awards[i]))
-			}
+			reactions = append(reactions, awards...)
 
 			if len(awards) < perPage {
 				break
@@ -614,7 +610,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
 			MergeCommitSHA: pr.MergeCommitSHA,
 			MergedTime:     mergeTime,
 			IsLocked:       locked,
-			Reactions:      reactions,
+			Reactions:      g.awardsToReactions(reactions),
 			Head: base.PullRequestBranch{
 				Ref:       pr.SourceBranch,
 				SHA:       pr.SHA,
@@ -675,10 +671,19 @@ func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie
 	return reviews, nil
 }
 
-func (g *GitlabDownloader) awardToReaction(award *gitlab.AwardEmoji) *base.Reaction {
-	return &base.Reaction{
-		UserID:   int64(award.User.ID),
-		UserName: award.User.Username,
-		Content:  award.Name,
+func (g *GitlabDownloader) awardsToReactions(awards []*gitlab.AwardEmoji) []*base.Reaction {
+	result := make([]*base.Reaction, 0, len(awards))
+	uniqCheck := make(map[string]struct{})
+	for _, award := range awards {
+		uid := fmt.Sprintf("%s%d", award.Name, award.User.ID)
+		if _, ok := uniqCheck[uid]; !ok {
+			result = append(result, &base.Reaction{
+				UserID:   int64(award.User.ID),
+				UserName: award.User.Username,
+				Content:  award.Name,
+			})
+			uniqCheck[uid] = struct{}{}
+		}
 	}
+	return result
 }
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 1d8c5989bb..731486eff2 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -13,6 +13,7 @@ import (
 	"testing"
 	"time"
 
+	"code.gitea.io/gitea/modules/json"
 	base "code.gitea.io/gitea/modules/migration"
 
 	"github.com/stretchr/testify/assert"
@@ -469,3 +470,49 @@ func TestGitlabGetReviews(t *testing.T) {
 		assertReviewsEqual(t, []*base.Review{&review}, rvs)
 	}
 }
+
+func TestAwardsToReactions(t *testing.T) {
+	downloader := &GitlabDownloader{}
+	// yes gitlab can have duplicated reactions (https://gitlab.com/jaywink/socialhome/-/issues/24)
+	testResponse := `
+[
+  {
+    "name": "thumbsup",
+    "user": {
+      "id": 1241334,
+      "username": "lafriks"
+    }
+  },
+  {
+    "name": "thumbsup",
+    "user": {
+      "id": 1241334,
+      "username": "lafriks"
+    }
+  },
+  {
+    "name": "thumbsup",
+    "user": {
+      "id": 4575606,
+      "username": "real6543"
+    }
+  }
+]
+`
+	var awards []*gitlab.AwardEmoji
+	assert.NoError(t, json.Unmarshal([]byte(testResponse), &awards))
+
+	reactions := downloader.awardsToReactions(awards)
+	assert.EqualValues(t, []*base.Reaction{
+		{
+			UserName: "lafriks",
+			UserID:   1241334,
+			Content:  "thumbsup",
+		},
+		{
+			UserName: "real6543",
+			UserID:   4575606,
+			Content:  "thumbsup",
+		},
+	}, reactions)
+}