test: add deployment rollback tests (closes #71) #78
@ -235,9 +235,9 @@ func (m *Middleware) CSRF() func(http.Handler) http.Handler {
|
||||
// loginRateLimit configures the login rate limiter.
|
||||
const (
|
||||
loginRateLimit = rate.Limit(5.0 / 60.0) // 5 requests per 60 seconds
|
||||
loginBurst = 5 // allow burst of 5
|
||||
limiterExpiry = 10 * time.Minute // evict entries not seen in 10 minutes
|
||||
limiterCleanupEvery = 1 * time.Minute // sweep interval
|
||||
loginBurst = 5 // allow burst of 5
|
||||
limiterExpiry = 10 * time.Minute // evict entries not seen in 10 minutes
|
||||
limiterCleanupEvery = 1 * time.Minute // sweep interval
|
||||
)
|
||||
|
||||
// ipLimiterEntry stores a rate limiter with its last-seen timestamp.
|
||||
@ -249,8 +249,8 @@ type ipLimiterEntry struct {
|
||||
// ipLimiter tracks per-IP rate limiters for login attempts with automatic
|
||||
// eviction of stale entries to prevent unbounded memory growth.
|
||||
type ipLimiter struct {
|
||||
mu sync.Mutex
|
||||
limiters map[string]*ipLimiterEntry
|
||||
mu sync.Mutex
|
||||
limiters map[string]*ipLimiterEntry
|
||||
lastSweep time.Time
|
||||
}
|
||||
|
||||
|
||||
@ -32,23 +32,23 @@ const (
|
||||
type App struct {
|
||||
db *database.Database
|
||||
|
||||
ID string
|
||||
Name string
|
||||
RepoURL string
|
||||
Branch string
|
||||
DockerfilePath string
|
||||
ID string
|
||||
Name string
|
||||
RepoURL string
|
||||
Branch string
|
||||
DockerfilePath string
|
||||
WebhookSecret string
|
||||
WebhookSecretHash string
|
||||
SSHPrivateKey string
|
||||
SSHPublicKey string
|
||||
ImageID sql.NullString
|
||||
PreviousImageID sql.NullString
|
||||
Status AppStatus
|
||||
DockerNetwork sql.NullString
|
||||
NtfyTopic sql.NullString
|
||||
SlackWebhook sql.NullString
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
SSHPublicKey string
|
||||
ImageID sql.NullString
|
||||
PreviousImageID sql.NullString
|
||||
Status AppStatus
|
||||
DockerNetwork sql.NullString
|
||||
NtfyTopic sql.NullString
|
||||
SlackWebhook sql.NullString
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewApp creates a new App with a database reference.
|
||||
|
||||
@ -54,48 +54,48 @@ func (s *Server) SetupRoutes() {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.mw.SessionAuth())
|
||||
|
||||
// Dashboard
|
||||
r.Get("/", s.handlers.HandleDashboard())
|
||||
// Dashboard
|
||||
r.Get("/", s.handlers.HandleDashboard())
|
||||
|
||||
// Logout
|
||||
r.Post("/logout", s.handlers.HandleLogout())
|
||||
// Logout
|
||||
r.Post("/logout", s.handlers.HandleLogout())
|
||||
|
||||
// App routes
|
||||
r.Get("/apps/new", s.handlers.HandleAppNew())
|
||||
r.Post("/apps", s.handlers.HandleAppCreate())
|
||||
r.Get("/apps/{id}", s.handlers.HandleAppDetail())
|
||||
r.Get("/apps/{id}/edit", s.handlers.HandleAppEdit())
|
||||
r.Post("/apps/{id}", s.handlers.HandleAppUpdate())
|
||||
r.Post("/apps/{id}/delete", s.handlers.HandleAppDelete())
|
||||
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
|
||||
r.Post("/apps/{id}/deployments/cancel", s.handlers.HandleCancelDeploy())
|
||||
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
|
||||
r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI())
|
||||
r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload())
|
||||
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
|
||||
r.Get("/apps/{id}/container-logs", s.handlers.HandleContainerLogsAPI())
|
||||
r.Get("/apps/{id}/status", s.handlers.HandleAppStatusAPI())
|
||||
r.Get("/apps/{id}/recent-deployments", s.handlers.HandleRecentDeploymentsAPI())
|
||||
r.Post("/apps/{id}/rollback", s.handlers.HandleAppRollback())
|
||||
r.Post("/apps/{id}/restart", s.handlers.HandleAppRestart())
|
||||
r.Post("/apps/{id}/stop", s.handlers.HandleAppStop())
|
||||
r.Post("/apps/{id}/start", s.handlers.HandleAppStart())
|
||||
// App routes
|
||||
r.Get("/apps/new", s.handlers.HandleAppNew())
|
||||
r.Post("/apps", s.handlers.HandleAppCreate())
|
||||
r.Get("/apps/{id}", s.handlers.HandleAppDetail())
|
||||
r.Get("/apps/{id}/edit", s.handlers.HandleAppEdit())
|
||||
r.Post("/apps/{id}", s.handlers.HandleAppUpdate())
|
||||
r.Post("/apps/{id}/delete", s.handlers.HandleAppDelete())
|
||||
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
|
||||
r.Post("/apps/{id}/deployments/cancel", s.handlers.HandleCancelDeploy())
|
||||
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
|
||||
r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI())
|
||||
r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload())
|
||||
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
|
||||
r.Get("/apps/{id}/container-logs", s.handlers.HandleContainerLogsAPI())
|
||||
r.Get("/apps/{id}/status", s.handlers.HandleAppStatusAPI())
|
||||
r.Get("/apps/{id}/recent-deployments", s.handlers.HandleRecentDeploymentsAPI())
|
||||
r.Post("/apps/{id}/rollback", s.handlers.HandleAppRollback())
|
||||
r.Post("/apps/{id}/restart", s.handlers.HandleAppRestart())
|
||||
r.Post("/apps/{id}/stop", s.handlers.HandleAppStop())
|
||||
r.Post("/apps/{id}/start", s.handlers.HandleAppStart())
|
||||
|
||||
// Environment variables
|
||||
r.Post("/apps/{id}/env-vars", s.handlers.HandleEnvVarAdd())
|
||||
r.Post("/apps/{id}/env-vars/{varID}/delete", s.handlers.HandleEnvVarDelete())
|
||||
// Environment variables
|
||||
r.Post("/apps/{id}/env-vars", s.handlers.HandleEnvVarAdd())
|
||||
r.Post("/apps/{id}/env-vars/{varID}/delete", s.handlers.HandleEnvVarDelete())
|
||||
|
||||
// Labels
|
||||
r.Post("/apps/{id}/labels", s.handlers.HandleLabelAdd())
|
||||
r.Post("/apps/{id}/labels/{labelID}/delete", s.handlers.HandleLabelDelete())
|
||||
// Labels
|
||||
r.Post("/apps/{id}/labels", s.handlers.HandleLabelAdd())
|
||||
r.Post("/apps/{id}/labels/{labelID}/delete", s.handlers.HandleLabelDelete())
|
||||
|
||||
// Volumes
|
||||
r.Post("/apps/{id}/volumes", s.handlers.HandleVolumeAdd())
|
||||
r.Post("/apps/{id}/volumes/{volumeID}/delete", s.handlers.HandleVolumeDelete())
|
||||
// Volumes
|
||||
r.Post("/apps/{id}/volumes", s.handlers.HandleVolumeAdd())
|
||||
r.Post("/apps/{id}/volumes/{volumeID}/delete", s.handlers.HandleVolumeDelete())
|
||||
|
||||
// Ports
|
||||
r.Post("/apps/{id}/ports", s.handlers.HandlePortAdd())
|
||||
r.Post("/apps/{id}/ports/{portID}/delete", s.handlers.HandlePortDelete())
|
||||
// Ports
|
||||
r.Post("/apps/{id}/ports", s.handlers.HandlePortAdd())
|
||||
r.Post("/apps/{id}/ports/{portID}/delete", s.handlers.HandlePortDelete())
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ type deploymentLogWriter struct {
|
||||
lineBuffer bytes.Buffer // buffer for incomplete lines
|
||||
mu sync.Mutex
|
||||
done chan struct{}
|
||||
flushed sync.WaitGroup // waits for flush goroutine to finish
|
||||
flushed sync.WaitGroup // waits for flush goroutine to finish
|
||||
flushCtx context.Context //nolint:containedctx // needed for async flush goroutine
|
||||
}
|
||||
|
||||
|
||||
74
internal/service/deploy/deploy_rollback_test.go
Normal file
74
internal/service/deploy/deploy_rollback_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package deploy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"git.eeqj.de/sneak/upaas/internal/models"
|
||||
"git.eeqj.de/sneak/upaas/internal/service/deploy"
|
||||
)
|
||||
|
||||
func TestRollback_NoPreviousImage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := deploy.NewTestService(slog.Default())
|
||||
app := &models.App{
|
||||
ID: "app-rollback-1",
|
||||
PreviousImageID: sql.NullString{},
|
||||
}
|
||||
|
||||
err := svc.Rollback(context.Background(), app)
|
||||
assert.ErrorIs(t, err, deploy.ErrNoPreviousImage)
|
||||
}
|
||||
|
||||
func TestRollback_EmptyPreviousImage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := deploy.NewTestService(slog.Default())
|
||||
app := &models.App{
|
||||
ID: "app-rollback-2",
|
||||
PreviousImageID: sql.NullString{String: "", Valid: true},
|
||||
}
|
||||
|
||||
err := svc.Rollback(context.Background(), app)
|
||||
assert.ErrorIs(t, err, deploy.ErrNoPreviousImage)
|
||||
}
|
||||
|
||||
func TestRollback_DeploymentLocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := deploy.NewTestService(slog.Default())
|
||||
|
||||
// Simulate a deploy holding the lock
|
||||
assert.True(t, svc.TryLockApp("app-rollback-3"))
|
||||
defer svc.UnlockApp("app-rollback-3")
|
||||
|
||||
app := &models.App{
|
||||
ID: "app-rollback-3",
|
||||
PreviousImageID: sql.NullString{String: "sha256:abc123", Valid: true},
|
||||
}
|
||||
|
||||
err := svc.Rollback(context.Background(), app)
|
||||
assert.ErrorIs(t, err, deploy.ErrDeploymentInProgress)
|
||||
}
|
||||
|
||||
func TestRollback_LockedApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := deploy.NewTestService(slog.Default())
|
||||
|
||||
assert.True(t, svc.TryLockApp("app-rollback-4"))
|
||||
defer svc.UnlockApp("app-rollback-4")
|
||||
|
||||
app := &models.App{
|
||||
ID: "app-rollback-4",
|
||||
PreviousImageID: sql.NullString{String: "sha256:abc123", Valid: true},
|
||||
}
|
||||
|
||||
err := svc.Rollback(context.Background(), app)
|
||||
assert.ErrorIs(t, err, deploy.ErrDeploymentInProgress)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user