Files
rogee 4d684a71eb
Build and publish Docker images / Build and publish images (push) Successful in 2m10s
feat: geolocate anonymous widget visitors
2026-09-15 08:54:13 +08:00

246 lines
8.1 KiB
Go

package service
import (
"context"
"encoding/json"
"net"
"testing"
"github.com/gochat/gochat/internal/geoip"
"github.com/gochat/gochat/internal/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeVisitorGeoResolver struct {
location geoip.Location
ok bool
seenIP net.IP
}
func (f *fakeVisitorGeoResolver) Lookup(ip net.IP) (geoip.Location, bool) {
f.seenIP = append(net.IP(nil), ip...)
return f.location, f.ok
}
func TestFormatVisitorName(t *testing.T) {
tests := []struct {
name string
province string
city string
want string
}{
{name: "province and city", province: "河北省", city: "保定市", want: "河北保定客户"},
{name: "municipality", province: "北京市", city: "北京市", want: "北京客户"},
{name: "province only", province: "河北省", want: "河北客户"},
{name: "empty", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, formatVisitorName(tt.province, tt.city))
})
}
}
func TestWidgetService_InitUsesGeoIPNameWithoutPersistingIP(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
_, inbox := seedWidgetInbox(t, db)
resolver := &fakeVisitorGeoResolver{
location: geoip.Location{Province: "河北省", City: "保定市"},
ok: true,
}
svc.SetVisitorGeoResolver(resolver)
resp, err := svc.Init(context.Background(), WidgetInitRequest{
WebsiteToken: "test_ws_token_123",
ClientIP: "203.0.113.10",
})
require.NoError(t, err)
require.NotNil(t, resp.Contact)
assert.Equal(t, inbox.ID, resp.InboxID)
assert.Equal(t, "203.0.113.10", resolver.seenIP.String())
assert.Equal(t, "河北保定客户", resp.Contact.Name)
var attributes map[string]any
require.NoError(t, json.Unmarshal(resp.Contact.AdditionalAttributes, &attributes))
assert.Equal(t, visitorNameSourceGeoIP, attributes[visitorNameSourceKey])
assert.Equal(t, "河北", attributes[visitorProvinceKey])
assert.Equal(t, "保定", attributes[visitorCityKey])
assert.NotContains(t, attributes, "client_ip")
assert.NotContains(t, attributes, "created_at_ip")
}
func TestWidgetService_InitFallsBackWhenGeoIPHasNoRecord(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
seedWidgetInbox(t, db)
svc.SetVisitorGeoResolver(&fakeVisitorGeoResolver{ok: false})
resp, err := svc.Init(context.Background(), WidgetInitRequest{
WebsiteToken: "test_ws_token_123",
ClientIP: "198.51.100.20",
})
require.NoError(t, err)
assert.Equal(t, anonymousVisitorName, resp.Contact.Name)
}
func TestWidgetService_InitPreservesRealName(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
seedWidgetInbox(t, db)
resolver := &fakeVisitorGeoResolver{location: geoip.Location{Province: "河北", City: "保定"}, ok: true}
svc.SetVisitorGeoResolver(resolver)
resp, err := svc.Init(context.Background(), WidgetInitRequest{
WebsiteToken: "test_ws_token_123",
ContactName: "张三",
ClientIP: "203.0.113.11",
})
require.NoError(t, err)
assert.Equal(t, "张三", resp.Contact.Name)
assert.Nil(t, resolver.seenIP)
}
func TestWidgetService_InitUpgradesLegacyAnonymousName(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
account, _ := seedWidgetInbox(t, db)
legacy := &model.Contact{
AccountID: account.ID,
Name: legacyAnonymousVisitorName,
Email: "legacy@example.com",
ContactType: "visitor",
}
require.NoError(t, db.Create(legacy).Error)
resolver := &fakeVisitorGeoResolver{location: geoip.Location{Province: "北京市", City: "北京市"}, ok: true}
svc.SetVisitorGeoResolver(resolver)
resp, err := svc.Init(context.Background(), WidgetInitRequest{
WebsiteToken: "test_ws_token_123",
ContactEmail: "legacy@example.com",
ClientIP: "203.0.113.12",
})
require.NoError(t, err)
assert.Equal(t, legacy.ID, resp.ContactID)
assert.Equal(t, "北京客户", resp.Contact.Name)
}
func TestWidgetService_RealNameWinsOverGeneratedName(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
account, _ := seedWidgetInbox(t, db)
contact := &model.Contact{
AccountID: account.ID,
Name: anonymousVisitorName,
AdditionalAttributes: mustJSON(map[string]any{
visitorNameSourceKey: visitorNameSourceFallback,
}),
}
require.NoError(t, db.Create(contact).Error)
svc.SetVisitorGeoResolver(&fakeVisitorGeoResolver{
location: geoip.Location{Province: "河北", City: "保定"},
ok: true,
})
updated, err := svc.updateContactFields(context.Background(), contact, WidgetContactUpdate{Name: "张三"})
require.NoError(t, err)
assert.Equal(t, "张三", updated.Name)
assert.Empty(t, jsonMap(updated.AdditionalAttributes)[visitorNameSourceKey])
updated, err = svc.enrichAnonymousWidgetContact(context.Background(), updated, "203.0.113.14")
require.NoError(t, err)
assert.Equal(t, "张三", updated.Name)
}
func TestWidgetService_DoesNotOverwriteManuallyRenamedFallbackContact(t *testing.T) {
_, svc := setupWidgetServiceTest(t)
contact := &model.Contact{
Name: "客服改名",
AdditionalAttributes: mustJSON(map[string]any{
visitorNameSourceKey: visitorNameSourceFallback,
}),
}
svc.SetVisitorGeoResolver(&fakeVisitorGeoResolver{
location: geoip.Location{Province: "河北", City: "保定"},
ok: true,
})
updated, err := svc.enrichAnonymousWidgetContact(context.Background(), contact, "203.0.113.15")
require.NoError(t, err)
assert.Equal(t, "客服改名", updated.Name)
}
func TestWidgetService_ProtectsVisitorAttributesFromClientUpdates(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
account, _ := seedWidgetInbox(t, db)
contact := &model.Contact{
AccountID: account.ID,
Name: "河北保定客户",
AdditionalAttributes: mustJSON(map[string]any{
visitorNameSourceKey: visitorNameSourceGeoIP,
visitorProvinceKey: "河北",
visitorCityKey: "保定",
}),
}
require.NoError(t, db.Create(contact).Error)
updated, err := svc.updateContactFields(context.Background(), contact, WidgetContactUpdate{
AdditionalAttributes: map[string]any{
visitorNameSourceKey: "spoofed",
visitorProvinceKey: "北京",
"customer_note": "kept",
},
})
require.NoError(t, err)
attributes := map[string]any{}
require.NoError(t, json.Unmarshal(updated.AdditionalAttributes, &attributes))
assert.Equal(t, visitorNameSourceGeoIP, attributes[visitorNameSourceKey])
assert.Equal(t, "河北", attributes[visitorProvinceKey])
assert.Equal(t, "kept", attributes["customer_note"])
}
func TestWidgetService_PublicContactUsesGeoIPNameAndFiltersClientMetadata(t *testing.T) {
db, svc := setupWidgetServiceTest(t)
account, _ := seedWidgetInbox(t, db)
svc.SetVisitorGeoResolver(&fakeVisitorGeoResolver{
location: geoip.Location{Province: "北京市", City: "北京市"},
ok: true,
})
contact, err := svc.findPublicContact(context.Background(), account.ID, PublicContactRequest{
AdditionalAttributes: map[string]any{
visitorNameSourceKey: "spoofed",
"customer_note": "kept",
},
ClientIP: "203.0.113.16",
})
require.NoError(t, err)
assert.Equal(t, "北京客户", contact.Name)
attributes := jsonMap(contact.AdditionalAttributes)
assert.Equal(t, visitorNameSourceGeoIP, attributes[visitorNameSourceKey])
assert.Equal(t, "kept", attributes["customer_note"])
}
func TestPublicWidgetAdditionalAttributesHidesVisitorMetadata(t *testing.T) {
public := PublicWidgetAdditionalAttributes(mustJSON(map[string]any{
visitorNameSourceKey: visitorNameSourceGeoIP,
visitorProvinceKey: "河北",
"created_at_ip": "203.0.113.10",
"customer_note": "kept",
}))
assert.Equal(t, map[string]any{"customer_note": "kept"}, public)
}
func TestWidgetService_SubmitOfflineMessageUsesGeoIPName(t *testing.T) {
db, svc := setupWidgetOfflineMessageTest(t)
account := createTestAccountForWidget(t, db)
inbox := createTestInboxForWidget(t, db, account.ID)
svc.SetVisitorGeoResolver(&fakeVisitorGeoResolver{
location: geoip.Location{Province: "河北省", City: "保定市"},
ok: true,
})
msg, err := svc.SubmitOfflineMessage(context.Background(), inbox.ID, account.ID, &model.WidgetOfflineMessageSubmission{
Message: "Need help",
ClientIP: "203.0.113.13",
}, "", "")
require.NoError(t, err)
assert.Equal(t, "河北保定客户", msg.ContactName)
}