2026-07-09 13:02:49 +08:00

297 lines
11 KiB
Go

package countryregion
import (
"context"
"database/sql"
"encoding/json"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
)
func TestRenameCountryCodeRejectsInvalidCode(t *testing.T) {
db, mock, closeDB := newCountryRenameMockDB(t)
defer closeDB()
store := &fakeCountryRenameJobStore{}
service := NewService(nil, db, store, nil)
_, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("P1"))
if !errors.Is(err, errCountryInvalid) {
t.Fatalf("expected invalid country error, got %v", err)
}
assertCountryRenameExpectations(t, mock)
if len(store.created) != 0 {
t.Fatalf("invalid request should not create job, got %d", len(store.created))
}
}
func TestRenameCountryCodeRejectsDuplicateCode(t *testing.T) {
db, mock, closeDB := newCountryRenameMockDB(t)
defer closeDB()
store := &fakeCountryRenameJobStore{}
service := NewService(nil, db, store, nil)
mock.ExpectBegin()
expectCountryRenameLockedCountry(mock, "PH")
expectCountryRenameDuplicateCode(mock, "PHL", 88)
mock.ExpectRollback()
_, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("PHL"))
if !errors.Is(err, errCountryConflict) {
t.Fatalf("expected duplicate country error, got %v", err)
}
assertCountryRenameExpectations(t, mock)
if len(store.created) != 0 {
t.Fatalf("duplicate country code should not create job, got %d", len(store.created))
}
}
func TestRenameCountryCodeRejectsActiveJobConflict(t *testing.T) {
db, mock, closeDB := newCountryRenameMockDB(t)
defer closeDB()
payload := countryCodeRenameJobPayload{
AppCode: "lalu",
CountryID: 12,
OldCountryCode: "PH",
NewCountryCode: "PHL",
}
body, _ := json.Marshal(payload)
store := &fakeCountryRenameJobStore{active: []model.AdminJob{{PayloadJSON: string(body)}}}
service := NewService(nil, db, store, nil)
mock.ExpectBegin()
expectCountryRenameLockedCountry(mock, "PH")
expectCountryRenameAvailableCode(mock, "PHL")
mock.ExpectRollback()
_, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("PHL"))
if !errors.Is(err, errCountryRenameJobConflict) {
t.Fatalf("expected active job conflict, got %v", err)
}
assertCountryRenameExpectations(t, mock)
if len(store.created) != 0 {
t.Fatalf("active conflict should not create job, got %d", len(store.created))
}
}
func TestRenameCountryCodeCreatesPendingJob(t *testing.T) {
db, mock, closeDB := newCountryRenameMockDB(t)
defer closeDB()
store := &fakeCountryRenameJobStore{}
service := NewService(nil, db, store, nil)
mock.ExpectBegin()
expectCountryRenameLockedCountry(mock, "PH")
expectCountryRenameAvailableCode(mock, "PHL")
mock.ExpectCommit()
resp, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("phl"))
if err != nil {
t.Fatalf("rename country code failed: %v", err)
}
assertCountryRenameExpectations(t, mock)
if resp.JobID != 101 || resp.Status != model.JobStatusPending || resp.OldCountryCode != "PH" || resp.NewCountryCode != "PHL" {
t.Fatalf("unexpected response: %+v", resp)
}
if len(store.created) != 1 {
t.Fatalf("expected one created job, got %d", len(store.created))
}
var payload countryCodeRenameJobPayload
if err := json.Unmarshal([]byte(store.created[0].PayloadJSON), &payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
if payload.CountryID != 12 || payload.OldCountryCode != "PH" || payload.NewCountryCode != "PHL" || payload.ActorID != 7 {
t.Fatalf("unexpected payload: %+v", payload)
}
}
func TestHandleCountryCodeRenameJobUpdatesCurrentStateAndReturnsCounts(t *testing.T) {
db, mock, closeDB := newCountryRenameMockDB(t)
defer closeDB()
store := &fakeCountryRenameJobStore{
adminCounts: repository.CountryCodeAdminReferenceCounts{Banners: 2, SplashScreens: 3},
}
room := &fakeCountryRenameRoomClient{
result: roomclient.RenameOwnerCountryCodeResult{RoomListEntries: 4, RoomSnapshots: 5},
}
service := NewService(nil, db, store, room)
mock.ExpectBegin()
expectCountryRenameLockedCountry(mock, "PH")
expectCountryRenameAvailableCode(mock, "PHL")
mock.ExpectExec("UPDATE countries").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("UPDATE region_countries").WillReturnResult(sqlmock.NewResult(0, 6))
mock.ExpectExec("UPDATE users").WillReturnResult(sqlmock.NewResult(0, 7))
mock.ExpectExec("UPDATE user_region_rebuild_tasks").WillReturnResult(sqlmock.NewResult(0, 8))
mock.ExpectCommit()
payload := countryCodeRenameJobPayload{
AppCode: "lalu",
CountryID: 12,
OldCountryCode: "PH",
NewCountryCode: "PHL",
CountryName: "Philippines",
CountryDisplayName: "菲律宾",
ActorID: 7,
RequestID: "req-1",
}
body, _ := json.Marshal(payload)
resultJSON, err := service.HandleCountryCodeRenameJob(context.Background(), &model.AdminJob{ID: 101, PayloadJSON: string(body)})
if err != nil {
t.Fatalf("handle job failed: %v", err)
}
assertCountryRenameExpectations(t, mock)
var result countryCodeRenameResult
if err := json.Unmarshal([]byte(resultJSON), &result); err != nil {
t.Fatalf("decode result: %v", err)
}
if result.Users != 7 || result.RegionMappings != 6 || result.RebuildTasks != 8 || result.Banners != 2 || result.SplashScreens != 3 || result.RoomListEntries != 4 || result.RoomSnapshots != 5 {
t.Fatalf("unexpected result counts: %+v", result)
}
if store.renamedAppCode != "lalu" || store.renamedOld != "PH" || store.renamedNew != "PHL" {
t.Fatalf("admin references not renamed with expected codes: %+v", store)
}
if room.req.OldCountryCode != "PH" || room.req.NewCountryCode != "PHL" || room.req.AdminID != 7 {
t.Fatalf("room rename request mismatch: %+v", room.req)
}
}
func TestHandleCountryCodeRenameJobIsIdempotentAfterUserDBAlreadyRenamed(t *testing.T) {
db, mock, closeDB := newCountryRenameMockDB(t)
defer closeDB()
store := &fakeCountryRenameJobStore{
adminCounts: repository.CountryCodeAdminReferenceCounts{Banners: 1, SplashScreens: 1},
}
room := &fakeCountryRenameRoomClient{
result: roomclient.RenameOwnerCountryCodeResult{RoomListEntries: 1, RoomSnapshots: 1},
}
service := NewService(nil, db, store, room)
mock.ExpectBegin()
expectCountryRenameLockedCountry(mock, "PHL")
expectCountryRenameAvailableCode(mock, "PHL")
mock.ExpectExec("UPDATE countries").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("UPDATE region_countries").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("UPDATE users").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("UPDATE user_region_rebuild_tasks").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectCommit()
payload := countryCodeRenameJobPayload{
AppCode: "lalu",
CountryID: 12,
OldCountryCode: "PH",
NewCountryCode: "PHL",
CountryName: "Philippines",
CountryDisplayName: "菲律宾",
ActorID: 7,
RequestID: "req-1",
}
body, _ := json.Marshal(payload)
resultJSON, err := service.HandleCountryCodeRenameJob(context.Background(), &model.AdminJob{ID: 101, PayloadJSON: string(body)})
if err != nil {
t.Fatalf("handle retry job failed: %v", err)
}
assertCountryRenameExpectations(t, mock)
var result countryCodeRenameResult
if err := json.Unmarshal([]byte(resultJSON), &result); err != nil {
t.Fatalf("decode result: %v", err)
}
if result.Users != 0 || result.RegionMappings != 0 || result.RebuildTasks != 0 || result.Banners != 1 || result.SplashScreens != 1 || result.RoomListEntries != 1 || result.RoomSnapshots != 1 {
t.Fatalf("unexpected idempotent result counts: %+v", result)
}
}
func validRenameCountryCodeRequest(countryCode string) renameCountryCodeRequest {
return renameCountryCodeRequest{
CountryName: "Philippines",
CountryCode: countryCode,
CountryDisplayName: "菲律宾",
ISOAlpha3: "PHL",
ISONumeric: "608",
PhoneCountryCode: "+63",
Flag: "PH",
SortOrder: 1770,
}
}
func newCountryRenameMockDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) {
t.Helper()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sqlmock: %v", err)
}
return db, mock, func() {
_ = db.Close()
}
}
func expectCountryRenameLockedCountry(mock sqlmock.Sqlmock, countryCode string) {
mock.ExpectQuery(`(?s)SELECT country_id, country_code.*FROM countries.*FOR UPDATE`).
WithArgs("lalu", int64(12)).
WillReturnRows(sqlmock.NewRows([]string{"country_id", "country_code"}).AddRow(12, countryCode))
}
func expectCountryRenameDuplicateCode(mock sqlmock.Sqlmock, countryCode string, existingID int64) {
mock.ExpectQuery(`(?s)SELECT country_id.*FROM countries.*country_id <>`).
WithArgs("lalu", countryCode, int64(12)).
WillReturnRows(sqlmock.NewRows([]string{"country_id"}).AddRow(existingID))
}
func expectCountryRenameAvailableCode(mock sqlmock.Sqlmock, countryCode string) {
mock.ExpectQuery(`(?s)SELECT country_id.*FROM countries.*country_id <>`).
WithArgs("lalu", countryCode, int64(12)).
WillReturnError(sql.ErrNoRows)
}
func assertCountryRenameExpectations(t *testing.T, mock sqlmock.Sqlmock) {
t.Helper()
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations: %v", err)
}
}
type fakeCountryRenameJobStore struct {
active []model.AdminJob
adminCounts repository.CountryCodeAdminReferenceCounts
created []model.AdminJob
renamedAppCode string
renamedOld string
renamedNew string
}
func (s *fakeCountryRenameJobStore) CreateJob(job *model.AdminJob) error {
job.ID = 101
job.Status = model.JobStatusPending
s.created = append(s.created, *job)
return nil
}
func (s *fakeCountryRenameJobStore) ListActiveJobsByType(string) ([]model.AdminJob, error) {
return s.active, nil
}
func (s *fakeCountryRenameJobStore) RenameCountryCodeAdminReferences(appCode string, oldCountryCode string, newCountryCode string, _ int64) (repository.CountryCodeAdminReferenceCounts, error) {
s.renamedAppCode = appCode
s.renamedOld = oldCountryCode
s.renamedNew = newCountryCode
return s.adminCounts, nil
}
type fakeCountryRenameRoomClient struct {
req roomclient.RenameOwnerCountryCodeRequest
result roomclient.RenameOwnerCountryCodeResult
}
func (c *fakeCountryRenameRoomClient) RenameOwnerCountryCode(_ context.Context, req roomclient.RenameOwnerCountryCodeRequest) (roomclient.RenameOwnerCountryCodeResult, error) {
c.req = req
return c.result, nil
}