Harden local config updates and error logging
This commit is contained in:
@@ -461,9 +461,21 @@ func (s *LocalConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigR
|
||||
|
||||
projectUUID := localCfg.ProjectUUID
|
||||
if req.ProjectUUID != nil {
|
||||
requestedProjectUUID := strings.TrimSpace(*req.ProjectUUID)
|
||||
currentProjectUUID := ""
|
||||
if localCfg.ProjectUUID != nil {
|
||||
currentProjectUUID = strings.TrimSpace(*localCfg.ProjectUUID)
|
||||
}
|
||||
|
||||
projectUUID, err = s.resolveProjectUUID(localCfg.OriginalUsername, req.ProjectUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Allow save for legacy/orphaned configs when request keeps the same project UUID.
|
||||
// This can happen for imported configs whose project is not present in local cache.
|
||||
if errors.Is(err, ErrProjectNotFound) && requestedProjectUUID != "" && requestedProjectUUID == currentProjectUUID {
|
||||
projectUUID = localCfg.ProjectUUID
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
@@ -1001,13 +1013,17 @@ func (s *LocalConfigurationService) saveWithVersionAndPending(localCfg *localdb.
|
||||
return fmt.Errorf("load current version before save: %w", err)
|
||||
}
|
||||
|
||||
sameRevisionContent, err := s.hasSameRevisionContent(localCfg, currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compare revision content: %w", err)
|
||||
}
|
||||
if sameRevisionContent {
|
||||
cfg = localdb.LocalToConfiguration(&locked)
|
||||
return nil
|
||||
// Legacy/orphaned rows may have empty or stale current_version_id.
|
||||
// In that case we treat update as content-changing and append a fresh version.
|
||||
if currentVersion != nil {
|
||||
sameRevisionContent, err := s.hasSameRevisionContent(localCfg, currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compare revision content: %w", err)
|
||||
}
|
||||
if sameRevisionContent {
|
||||
cfg = localdb.LocalToConfiguration(&locked)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,6 +1074,9 @@ func (s *LocalConfigurationService) loadCurrentVersionTx(tx *gorm.DB, localCfg *
|
||||
if err := tx.Where("configuration_uuid = ?", localCfg.UUID).
|
||||
Order("version_no DESC").
|
||||
First(&version).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &version, nil
|
||||
|
||||
@@ -290,6 +290,99 @@ func TestUpdateNoAuthKeepsProjectWhenProjectUUIDOmitted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNoAuthAllowsOrphanProjectWhenUUIDUnchanged(t *testing.T) {
|
||||
service, local := newLocalConfigServiceForTest(t)
|
||||
|
||||
project := &localdb.LocalProject{
|
||||
UUID: "project-orphan",
|
||||
OwnerUsername: "tester",
|
||||
Code: "TEST-ORPHAN",
|
||||
Name: ptrString("Orphan Project"),
|
||||
IsActive: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
SyncStatus: "synced",
|
||||
}
|
||||
if err := local.SaveProject(project); err != nil {
|
||||
t.Fatalf("save project: %v", err)
|
||||
}
|
||||
|
||||
created, err := service.Create("tester", &CreateConfigRequest{
|
||||
Name: "cfg",
|
||||
ProjectUUID: &project.UUID,
|
||||
Items: models.ConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 100}},
|
||||
ServerCount: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create config: %v", err)
|
||||
}
|
||||
|
||||
// Simulate missing project in local cache while config still references its UUID.
|
||||
if err := local.DB().Where("uuid = ?", project.UUID).Delete(&localdb.LocalProject{}).Error; err != nil {
|
||||
t.Fatalf("delete project: %v", err)
|
||||
}
|
||||
|
||||
updated, err := service.UpdateNoAuth(created.UUID, &CreateConfigRequest{
|
||||
Name: "cfg-updated",
|
||||
ProjectUUID: &project.UUID,
|
||||
Items: models.ConfigItems{{LotName: "CPU_A", Quantity: 2, UnitPrice: 100}},
|
||||
ServerCount: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update config with orphan project_uuid: %v", err)
|
||||
}
|
||||
if updated.ProjectUUID == nil || *updated.ProjectUUID != project.UUID {
|
||||
t.Fatalf("expected project_uuid to stay %s after update, got %+v", project.UUID, updated.ProjectUUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNoAuthRecoversWhenCurrentVersionMissing(t *testing.T) {
|
||||
service, local := newLocalConfigServiceForTest(t)
|
||||
|
||||
created, err := service.Create("tester", &CreateConfigRequest{
|
||||
Name: "cfg",
|
||||
Items: models.ConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 100}},
|
||||
ServerCount: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create config: %v", err)
|
||||
}
|
||||
|
||||
// Simulate corrupted/legacy versioning state:
|
||||
// local configuration exists, but all version rows are gone and pointer is stale.
|
||||
if err := local.DB().Where("configuration_uuid = ?", created.UUID).
|
||||
Delete(&localdb.LocalConfigurationVersion{}).Error; err != nil {
|
||||
t.Fatalf("delete versions: %v", err)
|
||||
}
|
||||
staleID := "missing-version-id"
|
||||
if err := local.DB().Model(&localdb.LocalConfiguration{}).
|
||||
Where("uuid = ?", created.UUID).
|
||||
Update("current_version_id", staleID).Error; err != nil {
|
||||
t.Fatalf("set stale current_version_id: %v", err)
|
||||
}
|
||||
|
||||
updated, err := service.UpdateNoAuth(created.UUID, &CreateConfigRequest{
|
||||
Name: "cfg-updated",
|
||||
Items: models.ConfigItems{{LotName: "CPU_A", Quantity: 2, UnitPrice: 100}},
|
||||
ServerCount: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update config with missing current version: %v", err)
|
||||
}
|
||||
|
||||
if updated.Name != "cfg-updated" {
|
||||
t.Fatalf("expected updated name, got %q", updated.Name)
|
||||
}
|
||||
|
||||
versions := loadVersions(t, local, created.UUID)
|
||||
if len(versions) != 1 {
|
||||
t.Fatalf("expected 1 recreated version, got %d", len(versions))
|
||||
}
|
||||
if versions[0].VersionNo != 1 {
|
||||
t.Fatalf("expected recreated version_no=1, got %d", versions[0].VersionNo)
|
||||
}
|
||||
}
|
||||
|
||||
func ptrString(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user