From c8edf2cb4dc675444b2cba285f9c642f069bc81c Mon Sep 17 00:00:00 2001
From: Vencislav Atanasov <user890104@users.noreply.github.com>
Date: Wed, 22 Nov 2023 23:52:16 +0200
Subject: [PATCH 03/10] Add tests for replying to UTF-8 messages, use runes to
 substring by character offset instead of byte offset (#418)
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

Co-authored-by: Tim Zabel <Tjzabel21@gmail.com>
Signed-off-by: Alexander Miroshnichenko <alex@millerson.name>
---
 internal/handlers/telegram/handler.go      |   4 +-
 internal/handlers/telegram/handler_test.go | 124 ++++++++++++++++-----
 internal/handlers/telegram/helpers.go      |   6 +-
 internal/handlers/telegram/helpers_test.go |  63 +++++++++--
 4 files changed, 156 insertions(+), 41 deletions(-)

diff --git a/internal/handlers/telegram/handler.go b/internal/handlers/telegram/handler.go
index 12404273adf4..2600b1dc42ef 100644
--- a/internal/handlers/telegram/handler.go
+++ b/internal/handlers/telegram/handler.go
@@ -97,8 +97,8 @@ func replyHandler(tg *Client, u tgbotapi.Update) {
 	replyUser := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.ReplyToMessage.From)
 
 	// Only show a portion of the reply text
-	if len(replyText) > tg.Settings.ReplyLength {
-		replyText = replyText[0:tg.Settings.ReplyLength] + "…"
+	if replyTextAsRunes := []rune(replyText); len(replyTextAsRunes) > tg.Settings.ReplyLength {
+		replyText = string(replyTextAsRunes[:tg.Settings.ReplyLength]) + "…"
 	}
 
 	formatted := fmt.Sprintf("%s%s%s %sRe %s: %s%s %s",
diff --git a/internal/handlers/telegram/handler_test.go b/internal/handlers/telegram/handler_test.go
index a2413c70d616..e356c5b54433 100644
--- a/internal/handlers/telegram/handler_test.go
+++ b/internal/handlers/telegram/handler_test.go
@@ -762,39 +762,107 @@ func TestMessageReply(t *testing.T) {
 	testChat := &tgbotapi.Chat{
 		ID: 100,
 	}
-	initMessage := &tgbotapi.Message{
-		From: testUser,
-		Text: "Initial Text",
-		Chat: testChat,
-	}
-	correct := "<replyUser> [Re test: Initial Text] Response Text"
 
-	updateObj := tgbotapi.Update{
-		Message: &tgbotapi.Message{
-			From:           replyUser,
-			Text:           "Response Text",
-			Chat:           testChat,
-			ReplyToMessage: initMessage,
+	tests := []struct {
+		name     string
+		updateFn func() tgbotapi.Update
+		expected string
+	}{
+		{
+			name: "ascii",
+			updateFn: func() tgbotapi.Update {
+				return tgbotapi.Update{
+					Message: &tgbotapi.Message{
+						From: replyUser,
+						Text: "Response Text",
+						Chat: testChat,
+						ReplyToMessage: &tgbotapi.Message{
+							From: testUser,
+							Text: "Initial Text",
+							Chat: testChat,
+						},
+					},
+				}
+			},
+			expected: "<replyUser> [Re test: Initial Text] Response Text",
 		},
-	}
-	clientObj := &Client{
-		Settings: &internal.TelegramSettings{
-			Prefix:      "<",
-			Suffix:      ">",
-			ReplyPrefix: "[",
-			ReplySuffix: "]",
-			ReplyLength: 15,
-			ChatID:      100,
+		{
+			name: "cyrillic-short",
+			updateFn: func() tgbotapi.Update {
+				return tgbotapi.Update{
+					Message: &tgbotapi.Message{
+						From: replyUser,
+						Text: "Response Text",
+						Chat: testChat,
+						ReplyToMessage: &tgbotapi.Message{
+							From: testUser,
+							Text: "Тест",
+							Chat: testChat,
+						},
+					},
+				}
+			},
+			expected: "<replyUser> [Re test: Тест] Response Text",
 		},
-		IRCSettings: &internal.IRCSettings{
-			ShowZWSP: false,
+		{
+			name: "cyrillic-long",
+			updateFn: func() tgbotapi.Update {
+				return tgbotapi.Update{
+					Message: &tgbotapi.Message{
+						From: replyUser,
+						Text: "Response Text",
+						Chat: testChat,
+						ReplyToMessage: &tgbotapi.Message{
+							From: testUser,
+							Text: "Уикипедия е свободна енциклопедия",
+							Chat: testChat,
+						},
+					},
+				}
+			},
+			expected: "<replyUser> [Re test: Уикипедия е сво…] Response Text",
 		},
-		sendToIrc: func(s string) {
-			assert.Equal(t, correct, s)
+		{
+			name: "japanese-long",
+			updateFn: func() tgbotapi.Update {
+				return tgbotapi.Update{
+					Message: &tgbotapi.Message{
+						From: replyUser,
+						Text: "Response Text",
+						Chat: testChat,
+						ReplyToMessage: &tgbotapi.Message{
+							From: testUser,
+							Text: "1234567テストテストテスト",
+							Chat: testChat,
+						},
+					},
+				}
+			},
+			expected: "<replyUser> [Re test: 1234567テストテストテス…] Response Text",
 		},
 	}
 
-	messageHandler(clientObj, updateObj)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			clientObj := &Client{
+				Settings: &internal.TelegramSettings{
+					Prefix:      "<",
+					Suffix:      ">",
+					ReplyPrefix: "[",
+					ReplySuffix: "]",
+					ReplyLength: 15,
+					ChatID:      100,
+				},
+				IRCSettings: &internal.IRCSettings{
+					ShowZWSP: false,
+				},
+				sendToIrc: func(actual string) {
+					assert.Equal(t, test.expected, actual)
+				},
+			}
+			messageHandler(clientObj, test.updateFn())
+		})
+	}
 }
 
 func TestMessageReplyZwsp(t *testing.T) {
@@ -891,8 +959,8 @@ func TestLocationHandlerWithLocationEnabled(t *testing.T) {
 		LastName:  "123",
 	}
 
-       // https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api#Location
-       location := &tgbotapi.Location{
+	// https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api#Location
+	location := &tgbotapi.Location{
 		Latitude:  43.0845274,
 		Longitude: -77.6781174,
 	}
diff --git a/internal/handlers/telegram/helpers.go b/internal/handlers/telegram/helpers.go
index f092979a9ffd..19b65ab722e3 100644
--- a/internal/handlers/telegram/helpers.go
+++ b/internal/handlers/telegram/helpers.go
@@ -37,7 +37,8 @@ Adds ZWSP to username to prevent username pinging across platform.
 func GetFullUserZwsp(u *tgbotapi.User) string {
 	// Add ZWSP to prevent pinging across platforms
 	// See https://github.com/42wim/matterbridge/issues/175
-	return u.FirstName + " (@" + u.UserName[:1] + "\u200b" + u.UserName[1:] + ")"
+	userNameAsRunes := []rune(u.UserName)
+	return u.FirstName + " (@" + string(userNameAsRunes[:1]) + "\u200b" + string(userNameAsRunes[1:]) + ")"
 }
 
 /*
@@ -47,7 +48,8 @@ username.
 func ZwspUsername(u *tgbotapi.User) string {
 	// Add ZWSP to prevent pinging across platforms
 	// See https://github.com/42wim/matterbridge/issues/175
-	return u.UserName[:1] + "\u200b" + u.UserName[1:]
+	userNameAsRunes := []rune(u.UserName)
+	return string(userNameAsRunes[:1]) + "\u200b" + string(userNameAsRunes[1:])
 }
 
 /*
diff --git a/internal/handlers/telegram/helpers_test.go b/internal/handlers/telegram/helpers_test.go
index 52eaa475c409..710f5a2c3ad7 100644
--- a/internal/handlers/telegram/helpers_test.go
+++ b/internal/handlers/telegram/helpers_test.go
@@ -16,12 +16,34 @@ func TestGetFullUsername(t *testing.T) {
 }
 
 func TestGetFullUserZwsp(t *testing.T) {
-	user := &tgbotapi.User{ID: 1, FirstName: "John", UserName: "jsmith"}
-	correct := user.FirstName + " (@" + user.UserName[:1] +
-		"\u200b" + user.UserName[1:] + ")"
-	name := GetFullUsername(true, user)
+	tests := []struct {
+		name     string
+		user     *tgbotapi.User
+		expected string
+	}{
+		{
+			name:     "ascii",
+			user:     &tgbotapi.User{ID: 1, FirstName: "John", UserName: "jsmith"},
+			expected: "John (@j\u200bsmith)",
+		},
+		{
+			name:     "cyrillic",
+			user:     &tgbotapi.User{ID: 1, FirstName: "Иван", UserName: "иван"},
+			expected: "Иван (@и\u200bван)",
+		},
+		{
+			name:     "japanese",
+			user:     &tgbotapi.User{ID: 1, FirstName: "まこと", UserName: "まこと"},
+			expected: "まこと (@ま\u200bこと)",
+		},
+	}
 
-	assert.Equal(t, correct, name)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetFullUsername(true, test.user)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
 }
 
 func TestGetFullNoUsername(t *testing.T) {
@@ -49,9 +71,32 @@ func TestGetUsername(t *testing.T) {
 }
 
 func TestZwspUsername(t *testing.T) {
-	user := &tgbotapi.User{ID: 1, FirstName: "John", UserName: "jsmith"}
-	correct := "j" + "\u200b" + "smith"
-	name := GetUsername(true, user)
+	tests := []struct {
+		name     string
+		user     *tgbotapi.User
+		expected string
+	}{
+		{
+			name:     "ascii",
+			user:     &tgbotapi.User{ID: 1, FirstName: "John", UserName: "jsmith"},
+			expected: "j\u200bsmith",
+		},
+		{
+			name:     "cyrillic",
+			user:     &tgbotapi.User{ID: 1, FirstName: "Иван", UserName: "иван"},
+			expected: "и\u200bван",
+		},
+		{
+			name:     "japanese",
+			user:     &tgbotapi.User{ID: 1, FirstName: "まこと", UserName: "まこと"},
+			expected: "ま\u200bこと",
+		},
+	}
 
-	assert.Equal(t, correct, name)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetUsername(true, test.user)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
 }
-- 
2.41.0

