2026-04-29 12:43:05 +08:00

186 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package grpc_test 验证 user-service gRPC 层的错误 reason 映射。
package grpc_test
import (
"context"
"testing"
"time"
userv1 "hyapp/api/proto/user/v1"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
authservice "hyapp/services/user-service/internal/service/auth"
userservice "hyapp/services/user-service/internal/service/user"
"hyapp/services/user-service/internal/testutil/mysqltest"
grpcserver "hyapp/services/user-service/internal/transport/grpc"
)
func seedCountry(t *testing.T, repository *mysqltest.Repository, code string) {
t.Helper()
if _, ok, err := repository.ResolveActiveCountryByCode(context.Background(), code); err != nil {
t.Fatalf("resolve country %s failed: %v", code, err)
} else if ok {
return
}
svc := userservice.New(repository)
if _, err := svc.CreateCountry(context.Background(), code+" Name", code, code+" Display", 0, 1, "seed-country-"+code); err != nil {
t.Fatalf("seed country %s failed: %v", code, err)
}
}
// TestLoginPasswordErrorCarriesReason 验证 transport 层把领域错误转换为 ErrorInfo.reason。
func TestLoginPasswordErrorCarriesReason(t *testing.T) {
// Auth service 没有完整输入时返回 INVALID_ARGUMENTtransport 必须转成 gRPC ErrorInfo.reason。
server := grpcserver.NewServer(authservice.New(authservice.Config{}), userservice.New(mysqltest.NewRepository(t)))
_, err := server.LoginPassword(context.Background(), &userv1.LoginPasswordRequest{
Meta: &userv1.RequestMeta{DeviceId: "ios"},
})
if got := xerr.ReasonFromGRPC(err); got != xerr.InvalidArgument {
t.Fatalf("reason mismatch: got %s", got)
}
}
func TestLoginThirdPartyMapsRegistrationProfile(t *testing.T) {
// gRPC 层负责把 request 字段和 meta fallback 收敛成 auth service 的注册快照。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "CN")
seedCountry(t, repository, "US")
authSvc := authservice.New(authservice.Config{
Issuer: "hyapp-test",
AccessTokenTTLSec: 1800,
RefreshTokenTTLSec: 2592000,
SigningAlg: "HS256",
SigningSecret: "test-secret",
}, authservice.WithRepository(repository), authservice.WithThirdPartyVerifier(authservice.NewStaticThirdPartyVerifier([]string{"wechat"})))
server := grpcserver.NewServer(authSvc, userservice.New(repository))
resp, err := server.LoginThirdParty(ctx, &userv1.LoginThirdPartyRequest{
Meta: &userv1.RequestMeta{RequestId: "req-third", DeviceId: "meta-device", ClientIp: "203.0.113.10", UserAgent: "hyapp-test/1.0", CountryByIp: "US"},
Provider: "wechat",
Credential: "openid-grpc",
Username: "hy",
Gender: "male",
Country: "CN",
InviteCode: "INV-1",
Ip: "198.51.100.9",
DeviceId: "req-device",
Device: "iPhone 15",
OsVersion: "iOS 18.1",
Avatar: "https://cdn.example/avatar.png",
Birth: "2000-01-02",
AppVersion: "1.2.3",
BuildNumber: "123",
Source: "campaign-a",
InstallChannel: "app_store",
Campaign: "spring_launch",
Platform: "ios",
Language: "zh-CN",
Timezone: "America/Los_Angeles",
})
if err != nil {
t.Fatalf("LoginThirdParty failed: %v", err)
}
user, err := repository.GetUser(ctx, resp.GetToken().GetUserId())
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.Username != "hy" || user.InviteCode != "INV-1" || user.RegisterIP != "203.0.113.10" || user.RegisterUserAgent != "hyapp-test/1.0" || user.CountryByIP != "US" || user.RegisterDeviceID != "req-device" {
t.Fatalf("registration fields were not mapped: %+v", user)
}
if user.RegisterDevice != "iPhone 15" || user.RegisterOSVersion != "iOS 18.1" || user.BirthDate != "2000-01-02" || user.RegisterAppVersion != "1.2.3" || user.RegisterBuildNumber != "123" || user.RegisterInstallChannel != "app_store" || user.RegisterCampaign != "spring_launch" || user.RegisterPlatform != "ios" || user.Timezone != "America/Los_Angeles" {
t.Fatalf("profile fields were not mapped: %+v", user)
}
}
func TestUpdateUserProfileAndCountry(t *testing.T) {
// UserService 新增资料写接口时gRPC 层必须保留 optional 字段的“未传/清空”语义。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "CN")
seedCountry(t, repository, "US")
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Country: "CN", Status: userdomain.StatusActive})
server := grpcserver.NewServer(authservice.New(authservice.Config{}), userservice.New(repository, userservice.WithCountryChangeCooldown(0)))
username := "hy"
avatar := "https://cdn.example/a.png"
birth := "2000-01-02"
profileResp, err := server.UpdateUserProfile(ctx, &userv1.UpdateUserProfileRequest{
UserId: 10001,
Username: &username,
Avatar: &avatar,
Birth: &birth,
})
if err != nil {
t.Fatalf("UpdateUserProfile failed: %v", err)
}
if profileResp.GetUser().GetUsername() != "hy" || profileResp.GetUser().GetAvatar() != avatar || profileResp.GetUser().GetBirth() != birth {
t.Fatalf("profile response mismatch: %+v", profileResp.GetUser())
}
countryResp, err := server.ChangeUserCountry(ctx, &userv1.ChangeUserCountryRequest{
Meta: &userv1.RequestMeta{RequestId: "req-country"},
UserId: 10001,
Country: "US",
})
if err != nil {
t.Fatalf("ChangeUserCountry failed: %v", err)
}
if countryResp.GetUser().GetCountry() != "US" {
t.Fatalf("country response mismatch: %+v", countryResp.GetUser())
}
}
func TestCountryAndRegionAdminRPC(t *testing.T) {
// 管理 RPC 只走 user-service 内部接口;公网暴露前 gateway 仍需补 admin auth。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := time.UnixMilli(1000)
userSvc := userservice.New(repository, userservice.WithClock(func() time.Time { return now }))
server := grpcserver.NewServer(authservice.New(authservice.Config{}), userSvc)
countryResp, err := server.CreateCountry(ctx, &userv1.CreateCountryRequest{
Meta: &userv1.RequestMeta{RequestId: "req-country"},
CountryName: "Example Area",
CountryCode: "xa",
CountryDisplayName: "测试区域",
SortOrder: 10,
OperatorUserId: 1,
})
if err != nil {
t.Fatalf("CreateCountry RPC failed: %v", err)
}
if countryResp.GetCountry().GetCountryCode() != "XA" || countryResp.GetCountry().GetCreatedAtMs() != 1000 {
t.Fatalf("country response mismatch: %+v", countryResp.GetCountry())
}
regionResp, err := server.CreateRegion(ctx, &userv1.CreateRegionRequest{
Meta: &userv1.RequestMeta{RequestId: "req-region"},
RegionCode: "sea",
Name: "Southeast Asia",
Countries: []string{"xa"},
OperatorUserId: 1,
})
if err != nil {
t.Fatalf("CreateRegion RPC failed: %v", err)
}
if regionResp.GetRegion().GetRegionCode() != "SEA" || len(regionResp.GetRegion().GetCountries()) != 1 || regionResp.GetRegion().GetCountries()[0] != "XA" {
t.Fatalf("region response mismatch: %+v", regionResp.GetRegion())
}
}
// TestResolveDisplayUserIDErrorCarriesReason 验证短号接口也使用 gRPC ErrorInfo.reason。
func TestResolveDisplayUserIDErrorCarriesReason(t *testing.T) {
// 短号格式错误也必须通过同一 gRPC 错误映射路径暴露稳定 reason。
server := grpcserver.NewServer(authservice.New(authservice.Config{}), userservice.New(mysqltest.NewRepository(t)))
_, err := server.ResolveDisplayUserID(context.Background(), &userv1.ResolveDisplayUserIDRequest{
DisplayUserId: "bad",
})
if got := xerr.ReasonFromGRPC(err); got != xerr.DisplayUserIDInvalid {
t.Fatalf("reason mismatch: got %s", got)
}
}