sync: recover missing server config during update push
This commit is contained in:
@@ -685,17 +685,36 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if localCfg.ServerID == nil {
|
if localCfg.ServerID == nil {
|
||||||
// Configuration hasn't been synced yet, try to find it on server by UUID
|
// Configuration hasn't been synced yet, try to find it on server by UUID.
|
||||||
serverCfg, err := configRepo.GetByUUID(cfg.UUID)
|
// If not found (e.g. stale create was skipped), create it from current snapshot.
|
||||||
if err != nil {
|
serverCfg, getErr := configRepo.GetByUUID(cfg.UUID)
|
||||||
return fmt.Errorf("configuration not yet synced to server: %w", err)
|
if getErr != nil {
|
||||||
|
if !errors.Is(getErr, gorm.ErrRecordNotFound) {
|
||||||
|
return fmt.Errorf("loading configuration from server: %w", getErr)
|
||||||
|
}
|
||||||
|
if createErr := configRepo.Create(&cfg); createErr != nil {
|
||||||
|
// Idempotency fallback: configuration may have been created concurrently.
|
||||||
|
existing, existingErr := configRepo.GetByUUID(cfg.UUID)
|
||||||
|
if existingErr != nil {
|
||||||
|
return fmt.Errorf("creating missing configuration on server: %w", createErr)
|
||||||
|
}
|
||||||
|
cfg.ID = existing.ID
|
||||||
|
}
|
||||||
|
if cfg.ID == 0 {
|
||||||
|
existing, existingErr := configRepo.GetByUUID(cfg.UUID)
|
||||||
|
if existingErr != nil {
|
||||||
|
return fmt.Errorf("loading created configuration from server: %w", existingErr)
|
||||||
|
}
|
||||||
|
cfg.ID = existing.ID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cfg.ID = serverCfg.ID
|
||||||
}
|
}
|
||||||
cfg.ID = serverCfg.ID
|
|
||||||
|
|
||||||
// Update local with server ID
|
// Update local with server ID
|
||||||
serverID := serverCfg.ID
|
serverID := cfg.ID
|
||||||
localCfg.ServerID = &serverID
|
localCfg.ServerID = &serverID
|
||||||
s.localDB.SaveConfiguration(localCfg)
|
s.localDB.SaveConfiguration(localCfg)
|
||||||
} else {
|
} else {
|
||||||
cfg.ID = *localCfg.ServerID
|
cfg.ID = *localCfg.ServerID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,57 @@ func TestPushPendingChangesCreateIsIdempotent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPushPendingChangesCreateThenUpdateBeforeFirstPush(t *testing.T) {
|
||||||
|
local := newLocalDBForSyncTest(t)
|
||||||
|
serverDB := newServerDBForSyncTest(t)
|
||||||
|
|
||||||
|
localSync := syncsvc.NewService(nil, local)
|
||||||
|
configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
|
||||||
|
pushService := syncsvc.NewServiceWithDB(serverDB, local)
|
||||||
|
|
||||||
|
created, err := configService.Create("tester", &services.CreateConfigRequest{
|
||||||
|
Name: "Cfg v1",
|
||||||
|
Items: models.ConfigItems{{LotName: "CPU_X", Quantity: 1, UnitPrice: 700}},
|
||||||
|
ServerCount: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := configService.UpdateNoAuth(created.UUID, &services.CreateConfigRequest{
|
||||||
|
Name: "Cfg v2",
|
||||||
|
Items: models.ConfigItems{{LotName: "CPU_X", Quantity: 3, UnitPrice: 700}},
|
||||||
|
ServerCount: 1,
|
||||||
|
ProjectUUID: created.ProjectUUID,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("update config before first push: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushed, err := pushService.PushPendingChanges()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("push pending changes: %v", err)
|
||||||
|
}
|
||||||
|
if pushed < 1 {
|
||||||
|
t.Fatalf("expected at least one pushed change, got %d", pushed)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverCfg models.Configuration
|
||||||
|
if err := serverDB.Where("uuid = ?", created.UUID).First(&serverCfg).Error; err != nil {
|
||||||
|
t.Fatalf("configuration not pushed to server: %v", err)
|
||||||
|
}
|
||||||
|
if serverCfg.Name != "Cfg v2" {
|
||||||
|
t.Fatalf("expected latest update to be pushed, got %q", serverCfg.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
localCfg, err := local.GetConfigurationByUUID(created.UUID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get local config: %v", err)
|
||||||
|
}
|
||||||
|
if localCfg.ServerID == nil || *localCfg.ServerID == 0 {
|
||||||
|
t.Fatalf("expected local configuration to have server_id after push, got %+v", localCfg.ServerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newLocalDBForSyncTest(t *testing.T) *localdb.LocalDB {
|
func newLocalDBForSyncTest(t *testing.T) *localdb.LocalDB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
localPath := filepath.Join(t.TempDir(), "local.db")
|
localPath := filepath.Join(t.TempDir(), "local.db")
|
||||||
|
|||||||
Reference in New Issue
Block a user