Fix all golangci-lint issues (closes #32) #34

Merged
sneak merged 1 commits from :fix/lint-cleanup into main 2026-02-16 06:57:20 +01:00
12 changed files with 241 additions and 157 deletions
Showing only changes of commit 559bfa4131 - Show all commits

View File

@ -172,6 +172,7 @@ func (d *Database) connect(ctx context.Context) error {
// HashWebhookSecret returns the hex-encoded SHA-256 hash of a webhook secret. // HashWebhookSecret returns the hex-encoded SHA-256 hash of a webhook secret.
func HashWebhookSecret(secret string) string { func HashWebhookSecret(secret string) string {
sum := sha256.Sum256([]byte(secret)) sum := sha256.Sum256([]byte(secret))
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
@ -181,6 +182,7 @@ func (d *Database) backfillWebhookSecretHashes(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("querying apps for backfill: %w", err) return fmt.Errorf("querying apps for backfill: %w", err)
} }
defer func() { _ = rows.Close() }() defer func() { _ = rows.Close() }()
type row struct { type row struct {
@ -191,14 +193,17 @@ func (d *Database) backfillWebhookSecretHashes(ctx context.Context) error {
for rows.Next() { for rows.Next() {
var r row var r row
if scanErr := rows.Scan(&r.id, &r.secret); scanErr != nil {
scanErr := rows.Scan(&r.id, &r.secret)
if scanErr != nil {
return fmt.Errorf("scanning app for backfill: %w", scanErr) return fmt.Errorf("scanning app for backfill: %w", scanErr)
} }
toUpdate = append(toUpdate, r) toUpdate = append(toUpdate, r)
} }
if rowsErr := rows.Err(); rowsErr != nil { rowsErr := rows.Err()
if rowsErr != nil {
return fmt.Errorf("iterating apps for backfill: %w", rowsErr) return fmt.Errorf("iterating apps for backfill: %w", rowsErr)
} }

View File

@ -611,10 +611,13 @@ func (c *Client) createGitContainer(
var script string var script string
if cfg.commitSHA != "" { if cfg.commitSHA != "" {
// Clone without depth limit so we can checkout any commit, then checkout specific SHA // Clone without depth limit so we can checkout any commit, then checkout specific SHA
script = `git clone --branch "$CLONE_BRANCH" "$CLONE_URL" /repo && cd /repo && git checkout "$CLONE_SHA" && echo COMMIT:$(git rev-parse HEAD)` script = `git clone --branch "$CLONE_BRANCH" "$CLONE_URL" /repo` +
` && cd /repo && git checkout "$CLONE_SHA"` +
` && echo COMMIT:$(git rev-parse HEAD)`
} else { } else {
// Shallow clone of branch HEAD, then output commit SHA // Shallow clone of branch HEAD, then output commit SHA
script = `git clone --depth 1 --branch "$CLONE_BRANCH" "$CLONE_URL" /repo && cd /repo && echo COMMIT:$(git rev-parse HEAD)` script = `git clone --depth 1 --branch "$CLONE_BRANCH" "$CLONE_URL" /repo` +
` && cd /repo && echo COMMIT:$(git rev-parse HEAD)`
} }
env := []string{ env := []string{

View File

@ -1,4 +1,4 @@
package docker package docker //nolint:testpackage // tests unexported regexps and Client struct
import ( import (
"errors" "errors"
@ -7,6 +7,8 @@ import (
) )
func TestValidBranchRegex(t *testing.T) { func TestValidBranchRegex(t *testing.T) {
t.Parallel()
valid := []string{ valid := []string{
"main", "main",
"develop", "develop",
@ -40,6 +42,8 @@ func TestValidBranchRegex(t *testing.T) {
} }
func TestValidCommitSHARegex(t *testing.T) { func TestValidCommitSHARegex(t *testing.T) {
t.Parallel()
valid := []string{ valid := []string{
"abc123def456789012345678901234567890abcd", "abc123def456789012345678901234567890abcd",
"0000000000000000000000000000000000000000", "0000000000000000000000000000000000000000",
@ -66,7 +70,9 @@ func TestValidCommitSHARegex(t *testing.T) {
} }
} }
func TestCloneRepoRejectsInjection(t *testing.T) { func TestCloneRepoRejectsInjection(t *testing.T) { //nolint:funlen // table-driven test
t.Parallel()
c := &Client{ c := &Client{
log: slog.Default(), log: slog.Default(),
} }
@ -119,6 +125,8 @@ func TestCloneRepoRejectsInjection(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := c.CloneRepo( _, err := c.CloneRepo(
t.Context(), t.Context(),
"git@example.com:repo.git", "git@example.com:repo.git",
@ -131,6 +139,7 @@ func TestCloneRepoRejectsInjection(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
} }
if !errors.Is(err, tt.wantErr) { if !errors.Is(err, tt.wantErr) {
t.Errorf("expected error %v, got %v", tt.wantErr, err) t.Errorf("expected error %v, got %v", tt.wantErr, err)
} }

View File

@ -255,6 +255,33 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc {
} }
} }
// cleanupContainer stops and removes the Docker container for the given app.
func (h *Handlers) cleanupContainer(ctx context.Context, appID, appName string) {
containerInfo, containerErr := h.docker.FindContainerByAppID(ctx, appID)
if containerErr != nil || containerInfo == nil {
return
}
if containerInfo.Running {
stopErr := h.docker.StopContainer(ctx, containerInfo.ID)
if stopErr != nil {
h.log.Error("failed to stop container during app deletion",
"error", stopErr, "app", appName,
"container", containerInfo.ID)
}
}
removeErr := h.docker.RemoveContainer(ctx, containerInfo.ID, true)
if removeErr != nil {
h.log.Error("failed to remove container during app deletion",
"error", removeErr, "app", appName,
"container", containerInfo.ID)
} else {
h.log.Info("removed container during app deletion",
"app", appName, "container", containerInfo.ID)
}
}
// HandleAppDelete handles app deletion. // HandleAppDelete handles app deletion.
func (h *Handlers) HandleAppDelete() http.HandlerFunc { func (h *Handlers) HandleAppDelete() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) {
@ -268,27 +295,7 @@ func (h *Handlers) HandleAppDelete() http.HandlerFunc {
} }
// Stop and remove the Docker container before deleting the DB record // Stop and remove the Docker container before deleting the DB record
containerInfo, containerErr := h.docker.FindContainerByAppID(request.Context(), appID) h.cleanupContainer(request.Context(), appID, application.Name)
if containerErr == nil && containerInfo != nil {
if containerInfo.Running {
stopErr := h.docker.StopContainer(request.Context(), containerInfo.ID)
if stopErr != nil {
h.log.Error("failed to stop container during app deletion",
"error", stopErr, "app", application.Name,
"container", containerInfo.ID)
}
}
removeErr := h.docker.RemoveContainer(request.Context(), containerInfo.ID, true)
if removeErr != nil {
h.log.Error("failed to remove container during app deletion",
"error", removeErr, "app", application.Name,
"container", containerInfo.ID)
} else {
h.log.Info("removed container during app deletion",
"app", application.Name, "container", containerInfo.ID)
}
}
deleteErr := application.Delete(request.Context()) deleteErr := application.Delete(request.Context())
if deleteErr != nil { if deleteErr != nil {
@ -1019,7 +1026,11 @@ func parsePortValues(hostPortStr, containerPortStr string) (int, int, bool) {
containerPort, containerErr := strconv.Atoi(containerPortStr) containerPort, containerErr := strconv.Atoi(containerPortStr)
const maxPort = 65535 const maxPort = 65535
if hostErr != nil || containerErr != nil || hostPort <= 0 || containerPort <= 0 || hostPort > maxPort || containerPort > maxPort {
invalid := hostErr != nil || containerErr != nil ||
hostPort <= 0 || containerPort <= 0 ||
hostPort > maxPort || containerPort > maxPort
if invalid {
return 0, 0, false return 0, 0, false
} }

View File

@ -3,7 +3,6 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"html/template"
"log/slog" "log/slog"
"net/http" "net/http"
@ -75,7 +74,7 @@ func (h *Handlers) addGlobals(
data["Appname"] = h.globals.Appname data["Appname"] = h.globals.Appname
if request != nil { if request != nil {
data["CSRFField"] = template.HTML(csrf.TemplateField(request)) //nolint:gosec // csrf.TemplateField produces safe HTML data["CSRFField"] = csrf.TemplateField(request)
} }
return data return data

View File

@ -450,8 +450,8 @@ func createTestApp(
return createdApp return createdApp
} }
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var // TestHandleWebhookRejectsOversizedBody tests that oversized webhook payloads
// via another app's URL path returns 404 (IDOR prevention). // are handled gracefully.
func TestHandleWebhookRejectsOversizedBody(t *testing.T) { func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
t.Parallel() t.Parallel()
@ -493,85 +493,113 @@ func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, http.StatusOK, recorder.Code)
} }
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var // ownedResourceTestConfig configures an IDOR ownership verification test.
// via another app's URL path returns 404 (IDOR prevention). type ownedResourceTestConfig struct {
func TestDeleteEnvVarOwnershipVerification(t *testing.T) { appPrefix1 string
t.Parallel() appPrefix2 string
createFn func(t *testing.T, tc *testContext, app *models.App) int64
deletePath func(appID string, resourceID int64) string
chiParams func(appID string, resourceID int64) map[string]string
handler func(h *handlers.Handlers) http.HandlerFunc
verifyFn func(t *testing.T, tc *testContext, resourceID int64)
}
func testOwnershipVerification(t *testing.T, cfg ownedResourceTestConfig) {
t.Helper()
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
app1 := createTestApp(t, testCtx, "envvar-owner-app") app1 := createTestApp(t, testCtx, cfg.appPrefix1)
app2 := createTestApp(t, testCtx, "envvar-other-app") app2 := createTestApp(t, testCtx, cfg.appPrefix2)
// Create env var belonging to app1 resourceID := cfg.createFn(t, testCtx, app1)
envVar := models.NewEnvVar(testCtx.database)
envVar.AppID = app1.ID request := httptest.NewRequest(
http.MethodPost,
cfg.deletePath(app2.ID, resourceID),
nil,
)
request = addChiURLParams(request, cfg.chiParams(app2.ID, resourceID))
recorder := httptest.NewRecorder()
handler := cfg.handler(testCtx.handlers)
handler.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotFound, recorder.Code)
cfg.verifyFn(t, testCtx, resourceID)
}
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var
// via another app's URL path returns 404 (IDOR prevention).
func TestDeleteEnvVarOwnershipVerification(t *testing.T) { //nolint:dupl // intentionally similar IDOR test pattern
t.Parallel()
testOwnershipVerification(t, ownedResourceTestConfig{
appPrefix1: "envvar-owner-app",
appPrefix2: "envvar-other-app",
createFn: func(t *testing.T, tc *testContext, ownerApp *models.App) int64 {
t.Helper()
envVar := models.NewEnvVar(tc.database)
envVar.AppID = ownerApp.ID
envVar.Key = "SECRET" envVar.Key = "SECRET"
envVar.Value = "hunter2" envVar.Value = "hunter2"
require.NoError(t, envVar.Save(context.Background())) require.NoError(t, envVar.Save(context.Background()))
// Try to delete app1's env var using app2's URL path return envVar.ID
request := httptest.NewRequest( },
http.MethodPost, deletePath: func(appID string, resourceID int64) string {
"/apps/"+app2.ID+"/env/"+strconv.FormatInt(envVar.ID, 10)+"/delete", return "/apps/" + appID + "/env/" + strconv.FormatInt(resourceID, 10) + "/delete"
nil, },
) chiParams: func(appID string, resourceID int64) map[string]string {
request = addChiURLParams(request, map[string]string{ return map[string]string{"id": appID, "envID": strconv.FormatInt(resourceID, 10)}
"id": app2.ID, },
"envID": strconv.FormatInt(envVar.ID, 10), handler: func(h *handlers.Handlers) http.HandlerFunc { return h.HandleEnvVarDelete() },
}) verifyFn: func(t *testing.T, tc *testContext, resourceID int64) {
recorder := httptest.NewRecorder() t.Helper()
handler := testCtx.handlers.HandleEnvVarDelete() found, findErr := models.FindEnvVar(context.Background(), tc.database, resourceID)
handler.ServeHTTP(recorder, request) require.NoError(t, findErr)
// Should return 404 because the env var doesn't belong to app2
assert.Equal(t, http.StatusNotFound, recorder.Code)
// Verify the env var was NOT deleted
found, err := models.FindEnvVar(context.Background(), testCtx.database, envVar.ID)
require.NoError(t, err)
assert.NotNil(t, found, "env var should still exist after IDOR attempt") assert.NotNil(t, found, "env var should still exist after IDOR attempt")
},
})
} }
// TestDeleteLabelOwnershipVerification tests that deleting a label // TestDeleteLabelOwnershipVerification tests that deleting a label
// via another app's URL path returns 404 (IDOR prevention). // via another app's URL path returns 404 (IDOR prevention).
func TestDeleteLabelOwnershipVerification(t *testing.T) { func TestDeleteLabelOwnershipVerification(t *testing.T) { //nolint:dupl // intentionally similar IDOR test pattern
t.Parallel() t.Parallel()
testCtx := setupTestHandlers(t) testOwnershipVerification(t, ownedResourceTestConfig{
appPrefix1: "label-owner-app",
appPrefix2: "label-other-app",
createFn: func(t *testing.T, tc *testContext, ownerApp *models.App) int64 {
t.Helper()
app1 := createTestApp(t, testCtx, "label-owner-app") lbl := models.NewLabel(tc.database)
app2 := createTestApp(t, testCtx, "label-other-app") lbl.AppID = ownerApp.ID
lbl.Key = "traefik.enable"
lbl.Value = "true"
require.NoError(t, lbl.Save(context.Background()))
// Create label belonging to app1 return lbl.ID
label := models.NewLabel(testCtx.database) },
label.AppID = app1.ID deletePath: func(appID string, resourceID int64) string {
label.Key = "traefik.enable" return "/apps/" + appID + "/labels/" + strconv.FormatInt(resourceID, 10) + "/delete"
label.Value = "true" },
require.NoError(t, label.Save(context.Background())) chiParams: func(appID string, resourceID int64) map[string]string {
return map[string]string{"id": appID, "labelID": strconv.FormatInt(resourceID, 10)}
},
handler: func(h *handlers.Handlers) http.HandlerFunc { return h.HandleLabelDelete() },
verifyFn: func(t *testing.T, tc *testContext, resourceID int64) {
t.Helper()
// Try to delete app1's label using app2's URL path found, findErr := models.FindLabel(context.Background(), tc.database, resourceID)
request := httptest.NewRequest( require.NoError(t, findErr)
http.MethodPost,
"/apps/"+app2.ID+"/labels/"+strconv.FormatInt(label.ID, 10)+"/delete",
nil,
)
request = addChiURLParams(request, map[string]string{
"id": app2.ID,
"labelID": strconv.FormatInt(label.ID, 10),
})
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleLabelDelete()
handler.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotFound, recorder.Code)
// Verify the label was NOT deleted
found, err := models.FindLabel(context.Background(), testCtx.database, label.ID)
require.NoError(t, err)
assert.NotNil(t, found, "label should still exist after IDOR attempt") assert.NotNil(t, found, "label should still exist after IDOR attempt")
},
})
} }
// TestDeleteVolumeOwnershipVerification tests that deleting a volume // TestDeleteVolumeOwnershipVerification tests that deleting a volume

View File

@ -1,8 +1,10 @@
package handlers package handlers //nolint:testpackage // tests unexported parsePortValues function
import "testing" import "testing"
func TestParsePortValues(t *testing.T) { func TestParsePortValues(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
name string name string
host string host string
@ -24,6 +26,8 @@ func TestParsePortValues(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
host, cont, valid := parsePortValues(tt.host, tt.container) host, cont, valid := parsePortValues(tt.host, tt.container)
if host != tt.wantHost || cont != tt.wantCont || valid != tt.wantValid { if host != tt.wantHost || cont != tt.wantCont || valid != tt.wantValid {
t.Errorf("parsePortValues(%q, %q) = (%d, %d, %v), want (%d, %d, %v)", t.Errorf("parsePortValues(%q, %q) = (%d, %d, %v), want (%d, %d, %v)",

View File

@ -2,11 +2,11 @@
package middleware package middleware
import ( import (
"fmt"
"log/slog" "log/slog"
"math" "math"
"net" "net"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -225,6 +225,7 @@ func (i *ipLimiter) sweep(now time.Time) {
delete(i.limiters, ip) delete(i.limiters, ip)
} }
} }
i.lastSweep = now i.lastSweep = now
} }
@ -246,6 +247,7 @@ func (i *ipLimiter) getLimiter(ip string) *rate.Limiter {
} }
i.limiters[ip] = entry i.limiters[ip] = entry
} }
entry.lastSeen = now entry.lastSeen = now
return entry.limiter return entry.limiter
@ -276,11 +278,9 @@ func (m *Middleware) LoginRateLimit() func(http.Handler) http.Handler {
reservation := limiter.Reserve() reservation := limiter.Reserve()
delay := reservation.Delay() delay := reservation.Delay()
reservation.Cancel() reservation.Cancel()
retryAfter := int(math.Ceil(delay.Seconds()))
if retryAfter < 1 { retryAfter := max(int(math.Ceil(delay.Seconds())), 1)
retryAfter = 1 writer.Header().Set("Retry-After", strconv.Itoa(retryAfter))
}
writer.Header().Set("Retry-After", fmt.Sprintf("%d", retryAfter))
http.Error( http.Error(
writer, writer,

View File

@ -1,4 +1,4 @@
package middleware package middleware //nolint:testpackage // tests unexported types and globals
import ( import (
"log/slog" "log/slog"
@ -23,6 +23,7 @@ func newTestMiddleware(t *testing.T) *Middleware {
} }
} }
//nolint:paralleltest // mutates global loginLimiter
func TestLoginRateLimitAllowsUpToBurst(t *testing.T) { func TestLoginRateLimitAllowsUpToBurst(t *testing.T) {
// Reset the global limiter to get clean state // Reset the global limiter to get clean state
loginLimiter = newIPLimiter() loginLimiter = newIPLimiter()
@ -50,6 +51,7 @@ func TestLoginRateLimitAllowsUpToBurst(t *testing.T) {
assert.Equal(t, http.StatusTooManyRequests, rec.Code, "6th request should be rate limited") assert.Equal(t, http.StatusTooManyRequests, rec.Code, "6th request should be rate limited")
} }
//nolint:paralleltest // mutates global loginLimiter
func TestLoginRateLimitIsolatesIPs(t *testing.T) { func TestLoginRateLimitIsolatesIPs(t *testing.T) {
loginLimiter = newIPLimiter() loginLimiter = newIPLimiter()
@ -82,6 +84,7 @@ func TestLoginRateLimitIsolatesIPs(t *testing.T) {
assert.Equal(t, http.StatusOK, rec2.Code, "different IP should not be rate limited") assert.Equal(t, http.StatusOK, rec2.Code, "different IP should not be rate limited")
} }
//nolint:paralleltest // mutates global loginLimiter
func TestLoginRateLimitReturns429Body(t *testing.T) { func TestLoginRateLimitReturns429Body(t *testing.T) {
loginLimiter = newIPLimiter() loginLimiter = newIPLimiter()
@ -109,6 +112,8 @@ func TestLoginRateLimitReturns429Body(t *testing.T) {
} }
func TestIPLimiterEvictsStaleEntries(t *testing.T) { func TestIPLimiterEvictsStaleEntries(t *testing.T) {
t.Parallel()
il := newIPLimiter() il := newIPLimiter()
// Add an entry and backdate its lastSeen // Add an entry and backdate its lastSeen
@ -130,6 +135,7 @@ func TestIPLimiterEvictsStaleEntries(t *testing.T) {
il.mu.Lock() il.mu.Lock()
defer il.mu.Unlock() defer il.mu.Unlock()
assert.NotContains(t, il.limiters, "1.2.3.4", "stale entry should be evicted") assert.NotContains(t, il.limiters, "1.2.3.4", "stale entry should be evicted")
assert.Contains(t, il.limiters, "5.6.7.8", "fresh entry should remain") assert.Contains(t, il.limiters, "5.6.7.8", "fresh entry should remain")
} }

View File

@ -1,11 +1,14 @@
package middleware package middleware //nolint:testpackage // tests unexported realIP function
import ( import (
"context"
"net/http" "net/http"
"testing" "testing"
) )
func TestRealIP(t *testing.T) { func TestRealIP(t *testing.T) { //nolint:funlen // table-driven test
t.Parallel()
tests := []struct { tests := []struct {
name string name string
remoteAddr string remoteAddr string
@ -65,11 +68,15 @@ func TestRealIP(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/", nil) t.Parallel()
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
req.RemoteAddr = tt.remoteAddr req.RemoteAddr = tt.remoteAddr
if tt.xRealIP != "" { if tt.xRealIP != "" {
req.Header.Set("X-Real-IP", tt.xRealIP) req.Header.Set("X-Real-IP", tt.xRealIP)
} }
if tt.xff != "" { if tt.xff != "" {
req.Header.Set("X-Forwarded-For", tt.xff) req.Header.Set("X-Forwarded-For", tt.xff)
} }

View File

@ -71,11 +71,8 @@ func setupTestService(t *testing.T) (*auth.Service, func()) {
return svc, cleanup return svc, cleanup
} }
func TestSessionCookieSecureFlag(testingT *testing.T) { func setupAuthService(t *testing.T, debug bool) *auth.Service {
testingT.Parallel() t.Helper()
testingT.Run("secure flag is true when debug is false", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir() tmpDir := t.TempDir()
@ -95,7 +92,7 @@ func TestSessionCookieSecureFlag(testingT *testing.T) {
Port: 8080, Port: 8080,
DataDir: tmpDir, DataDir: tmpDir,
SessionSecret: "test-secret-key-at-least-32-chars", SessionSecret: "test-secret-key-at-least-32-chars",
Debug: false, Debug: debug,
} }
dbInst, err := database.New(fx.Lifecycle(nil), database.Params{ dbInst, err := database.New(fx.Lifecycle(nil), database.Params{
@ -111,8 +108,13 @@ func TestSessionCookieSecureFlag(testingT *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
// Create user and session, check cookie has Secure flag return svc
_, err = svc.CreateUser(context.Background(), "admin", "password123") }
func getSessionCookie(t *testing.T, svc *auth.Service) *http.Cookie {
t.Helper()
_, err := svc.CreateUser(context.Background(), "admin", "password123")
require.NoError(t, err) require.NoError(t, err)
user, err := svc.Authenticate(context.Background(), "admin", "password123") user, err := svc.Authenticate(context.Background(), "admin", "password123")
@ -124,18 +126,25 @@ func TestSessionCookieSecureFlag(testingT *testing.T) {
err = svc.CreateSession(recorder, request, user) err = svc.CreateSession(recorder, request, user)
require.NoError(t, err) require.NoError(t, err)
cookies := recorder.Result().Cookies() for _, c := range recorder.Result().Cookies() {
require.NotEmpty(t, cookies)
var sessionCookie *http.Cookie
for _, c := range cookies {
if c.Name == "upaas_session" { if c.Name == "upaas_session" {
sessionCookie = c return c
break
} }
} }
require.NotNil(t, sessionCookie, "session cookie should exist")
assert.True(t, sessionCookie.Secure, "session cookie should have Secure flag in production mode") return nil
}
func TestSessionCookieSecureFlag(testingT *testing.T) {
testingT.Parallel()
testingT.Run("secure flag is true when debug is false", func(t *testing.T) {
t.Parallel()
svc := setupAuthService(t, false)
cookie := getSessionCookie(t, svc)
require.NotNil(t, cookie, "session cookie should exist")
assert.True(t, cookie.Secure, "session cookie should have Secure flag in production mode")
}) })
} }
@ -302,12 +311,14 @@ func TestCreateUserRaceCondition(testingT *testing.T) {
close(start) close(start)
var successes, failures int var successes, failures int
for range goroutines { for range goroutines {
err := <-results err := <-results
if err == nil { if err == nil {
successes++ successes++
} else { } else {
assert.ErrorIs(t, err, auth.ErrUserExists) require.ErrorIs(t, err, auth.ErrUserExists)
failures++ failures++
} }
} }

View File

@ -89,6 +89,7 @@ func newDeploymentLogWriter(ctx context.Context, deployment *models.Deployment)
flushCtx: ctx, flushCtx: ctx,
} }
w.flushed.Add(1) w.flushed.Add(1)
go w.runFlushLoop() go w.runFlushLoop()
return w return w