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) }