297 lines
11 KiB
Go
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
|
|
}
|