package models_test import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" "rocketgit.ru/rsmon/worker/app/models" ) func TestServer_RotateToken(t *testing.T) { models.Drop() models.Migrate() acc := &models.Account{Name: "rotate-token-account"} require.NoError(t, models.DB().Create(acc).Error) srv := &models.Server{ AccountID: acc.ID, Name: "rotate-target", Slug: "rotate-target", Region: "local", } require.NoError(t, models.DB().Create(srv).Error) t1 := models.GenerateServerToken() require.NoError(t, models.RotateServerToken(nil, srv.ID, t1)) got, err := models.FindServerByToken(t1) require.NoError(t, err) assert.Equal(t, srv.ID, got.ID) t2 := models.GenerateServerToken() require.NoError(t, models.RotateServerToken(nil, srv.ID, t2)) // Old token no longer matches. _, err = models.FindServerByToken(t1) assert.ErrorIs(t, err, gorm.ErrRecordNotFound) got2, err := models.FindServerByToken(t2) require.NoError(t, err) assert.Equal(t, srv.ID, got2.ID) // Generated tokens are hex-encoded 32 bytes (64 chars). assert.Len(t, t1, 64) assert.NotEqual(t, t1, t2) } func TestServer_InventoryFields_Default(t *testing.T) { models.Drop() models.Migrate() acc := &models.Account{Name: "inv-defaults"} require.NoError(t, models.DB().Create(acc).Error) srv := &models.Server{ AccountID: acc.ID, Name: "fresh-server", Slug: "fresh-server", Region: "local", } require.NoError(t, models.DB().Create(srv).Error) got := models.Server{} require.NoError(t, models.DB().First(&got, srv.ID).Error) assert.Equal(t, models.ServerKindProduction, got.Kind) assert.Equal(t, 0, got.PriceCents) assert.False(t, got.Paused) } func TestServer_KindEnum_OnlyAllowsValidValues(t *testing.T) { models.Drop() models.Migrate() acc := &models.Account{Name: "inv-enum"} require.NoError(t, models.DB().Create(acc).Error) srv := &models.Server{ AccountID: acc.ID, Name: "kinder", Slug: "kinder", Region: "local", Kind: models.ServerKindStaging, } require.NoError(t, models.DB().Create(srv).Error) got := models.Server{} require.NoError(t, models.DB().First(&got, srv.ID).Error) assert.Equal(t, models.ServerKindStaging, got.Kind) // Inserting an invalid value via raw SQL fails the enum check. err := models.DB().Exec( "INSERT INTO servers (account_id, name, slug, region, kind) VALUES (?, ?, ?, ?, ?)", acc.ID, "bad-kinder", "bad-kinder", "local", "scrapped", ).Error require.Error(t, err) assert.Contains(t, err.Error(), "invalid input value for enum") } func TestSite_Slugify(t *testing.T) { cases := []struct { in, want string }{ {"Cafe", "cafe"}, // cyrillic stripped (latin-only rule, see Slugify) {" Spaces Everywhere ", "spaces-everywhere"}, {"dots.and-dashes_and spaces", "dots-and-dashes-and-spaces"}, {"", "site"}, {"-leading-and-trailing-", "leading-and-trailing"}, {"mix_of.dots-dashes spaces", "mix-of-dots-dashes-spaces"}, } for _, c := range cases { t.Run(c.in, func(t *testing.T) { assert.Equal(t, c.want, models.SiteSlugify(c.in)) }) } } func TestSite_FindOrCreateBySlug(t *testing.T) { models.Drop() models.Migrate() acc := &models.Account{Name: "site-foc"} require.NoError(t, models.DB().Create(acc).Error) got, err := models.FindOrCreateSiteBySlug(nil, acc.ID, "my-site") require.NoError(t, err) require.NotZero(t, got.ID, "row should be persisted") assert.Equal(t, "my-site", got.Slug) assert.Equal(t, "production", got.Kind) assert.True(t, got.IsActive) // Second call returns the same row (idempotent). got2, err := models.FindOrCreateSiteBySlug(nil, acc.ID, "my-site") require.NoError(t, err) assert.Equal(t, got.ID, got2.ID) } func TestDeployment_UpsertNginx_MatchesByConfigPath(t *testing.T) { models.Drop() models.Migrate() acc := &models.Account{Name: "dep-upsert"} require.NoError(t, models.DB().Create(acc).Error) srv := &models.Server{ AccountID: acc.ID, Name: "host1", Slug: "host1", Region: "local", } require.NoError(t, models.DB().Create(srv).Error) d1, err := models.UpsertNginxDeployment(nil, acc.ID, srv.ID, "/etc/nginx/sites-enabled/a.conf") require.NoError(t, err) require.NoError(t, models.DB().Save(d1).Error) // Second call with the same config_path returns the existing row. d2, err := models.UpsertNginxDeployment(nil, acc.ID, srv.ID, "/etc/nginx/sites-enabled/a.conf") require.NoError(t, err) assert.Equal(t, d1.ID, d2.ID, "should reuse the same row on identical config_path") // Different config_path creates a new row. d3, err := models.UpsertNginxDeployment(nil, acc.ID, srv.ID, "/etc/nginx/sites-enabled/b.conf") require.NoError(t, err) assert.NotEqual(t, d1.ID, d3.ID) } func TestDeployment_ReconcileMissing_FlipsAction(t *testing.T) { models.Drop() models.Migrate() acc := &models.Account{Name: "reconcile"} require.NoError(t, models.DB().Create(acc).Error) srv := &models.Server{ AccountID: acc.ID, Name: "rec", Slug: "rec", Region: "local", } require.NoError(t, models.DB().Create(srv).Error) old := time.Now().Add(-2 * time.Hour) fresh := models.Deployment{ AccountID: acc.ID, ServerID: &srv.ID, Kind: models.DeploymentKindProduction, Mode: models.DeploymentModeDedicated, Action: models.DeploymentActionOk, ConfigPath: ptr("/etc/nginx/old.conf"), LastSeenAt: &old, } require.NoError(t, models.DB().Create(&fresh).Error) recent := models.Deployment{ AccountID: acc.ID, ServerID: &srv.ID, Kind: models.DeploymentKindProduction, Mode: models.DeploymentModeDedicated, Action: models.DeploymentActionOk, ConfigPath: ptr("/etc/nginx/recent.conf"), LastSeenAt: ptrTime(time.Now()), } require.NoError(t, models.DB().Create(&recent).Error) cutoff := time.Now().Add(-90 * time.Second) marked, err := models.ReconcileMissingDeployments(srv.ID, models.DeploymentModeDedicated, cutoff) require.NoError(t, err) assert.EqualValues(t, 1, marked, "only the old row should flip") var oldAfter models.Deployment require.NoError(t, models.DB().First(&oldAfter, fresh.ID).Error) assert.Equal(t, models.DeploymentActionMissing, oldAfter.Action) var recentAfter models.Deployment require.NoError(t, models.DB().First(&recentAfter, recent.ID).Error) assert.Equal(t, models.DeploymentActionOk, recentAfter.Action, "fresh row stays ok") // Re-running with the same cutoff is a no-op. marked2, err := models.ReconcileMissingDeployments(srv.ID, models.DeploymentModeDedicated, cutoff) require.NoError(t, err) assert.EqualValues(t, 0, marked2) } func ptr(s string) *string { return &s } func ptrTime(t time.Time) *time.Time { return &t }