From d4310faa8b55556eb12a358aec287573640f9f90 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Tue, 18 Aug 2026 09:13:13 +0330 Subject: [PATCH 1/7] fix(CubeOps/internal/store/db.go): fixing up the rejecting an empty JWT signing secret Signed-off-by: Dwin Gharibi --- CubeOps/internal/auth/jwt.go | 21 +++++++++++++++++++++ CubeOps/internal/store/db.go | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/CubeOps/internal/auth/jwt.go b/CubeOps/internal/auth/jwt.go index 2b602b736..352bba50e 100644 --- a/CubeOps/internal/auth/jwt.go +++ b/CubeOps/internal/auth/jwt.go @@ -59,6 +59,15 @@ func NewJWTManager(secret string, accessTTL, refreshTTL time.Duration) *JWTManag } } +var errEmptyJWTSecret = errors.New("jwt: signing secret is empty") + +func (m *JWTManager) checkSecret() error { + if len(m.secret) == 0 { + return errEmptyJWTSecret + } + return nil +} + // AccessTTL returns the configured access-token TTL. func (m *JWTManager) AccessTTL() time.Duration { return m.accessTTL } @@ -67,6 +76,9 @@ func (m *JWTManager) RefreshTTL() time.Duration { return m.refreshTTL } // GenerateAccessToken creates a signed JWT access token. func (m *JWTManager) GenerateAccessToken(username string) (string, error) { + if err := m.checkSecret(); err != nil { + return "", err + } now := time.Now() claims := AccessClaims{ RegisteredClaims: jwt.RegisteredClaims{ @@ -86,6 +98,9 @@ func (m *JWTManager) GenerateAccessToken(username string) (string, error) { // GenerateRefreshToken creates a signed JWT refresh token. func (m *JWTManager) GenerateRefreshToken(username string) (string, string, error) { + if err := m.checkSecret(); err != nil { + return "", "", err + } now := time.Now() tokenID := uuid.New().String() claims := RefreshClaims{ @@ -111,6 +126,9 @@ func (m *JWTManager) GenerateRefreshToken(username string) (string, string, erro // tokens by checking the "typ" claim and the audience, so a long-lived // refresh token cannot be used as an access token. func (m *JWTManager) VerifyAccessToken(tokenStr string) (*AccessClaims, error) { + if err := m.checkSecret(); err != nil { + return nil, err + } token, err := jwt.ParseWithClaims(tokenStr, &AccessClaims{}, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) @@ -136,6 +154,9 @@ func (m *JWTManager) VerifyAccessToken(tokenStr string) (*AccessClaims, error) { // than on this package's internals. It rejects access tokens via the "typ" // claim and the audience. func (m *JWTManager) VerifyRefreshToken(tokenStr string) (*service.RefreshClaims, error) { + if err := m.checkSecret(); err != nil { + return nil, err + } token, err := jwt.ParseWithClaims(tokenStr, &RefreshClaims{}, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) diff --git a/CubeOps/internal/store/db.go b/CubeOps/internal/store/db.go index 8178056ff..c2cfb092b 100644 --- a/CubeOps/internal/store/db.go +++ b/CubeOps/internal/store/db.go @@ -5,6 +5,7 @@ package store import ( "context" + "errors" "fmt" "github.com/tencentcloud/CubeSandbox/CubeDB/dao" @@ -140,6 +141,9 @@ func (s *Store) BootstrapJWTSecret(ctx context.Context, envSecret string) (strin if err != nil { return "", fmt.Errorf("persist JWT secret: %w", err) } + if winner == "" { + return "", errors.New("JWT secret resolved to an empty value in t_system_setting") + } if winner == generated { logging.G(ctx).Info("JWT secret auto-generated and persisted to database (t_system_setting)") } else { From 3e6771ed5b7f2a442b5701865a25841a7f91dbf6 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Tue, 18 Aug 2026 09:14:34 +0330 Subject: [PATCH 2/7] test(CubeOps/internal/auth/jwt_empty_secret_test.go): adding up some amazing tests for covering the fixed bug Signed-off-by: Dwin Gharibi --- .../internal/auth/jwt_empty_secret_test.go | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 CubeOps/internal/auth/jwt_empty_secret_test.go diff --git a/CubeOps/internal/auth/jwt_empty_secret_test.go b/CubeOps/internal/auth/jwt_empty_secret_test.go new file mode 100644 index 000000000..ff6e052b7 --- /dev/null +++ b/CubeOps/internal/auth/jwt_empty_secret_test.go @@ -0,0 +1,54 @@ +// Copyright (c) 2026 Tencent Inc. +// SPDX-License-Identifier: Apache-2.0 + +package auth_test + +import ( + "testing" + "time" + + "github.com/tencentcloud/CubeSandbox/CubeOps/internal/auth" +) + +func TestEmptySecretRejectedOnGenerate(t *testing.T) { + jm := auth.NewJWTManager("", 15*time.Minute, 168*time.Hour) + + if _, err := jm.GenerateAccessToken("admin"); err == nil { + t.Fatal("GenerateAccessToken accepted an empty signing secret") + } + if _, _, err := jm.GenerateRefreshToken("admin"); err == nil { + t.Fatal("GenerateRefreshToken accepted an empty signing secret") + } +} + +func TestEmptySecretRejectedOnVerify(t *testing.T) { + signer := auth.NewJWTManager("forged-secret-32-bytes-long-ok!!", 15*time.Minute, 168*time.Hour) + minted, err := signer.GenerateAccessToken("admin") + if err != nil { + t.Fatalf("failed to mint a token for the test: %v", err) + } + + empty := auth.NewJWTManager("", 15*time.Minute, 168*time.Hour) + if _, err := empty.VerifyAccessToken(minted); err == nil { + t.Fatal("VerifyAccessToken accepted a token while configured with an empty secret") + } + if _, err := empty.VerifyRefreshToken(minted); err == nil { + t.Fatal("VerifyRefreshToken accepted a token while configured with an empty secret") + } +} + +func TestNonEmptySecretStillWorks(t *testing.T) { + jm := auth.NewJWTManager("good-secret-32-bytes-long-enough!", 15*time.Minute, 168*time.Hour) + + access, err := jm.GenerateAccessToken("admin") + if err != nil { + t.Fatalf("GenerateAccessToken: %v", err) + } + claims, err := jm.VerifyAccessToken(access) + if err != nil { + t.Fatalf("VerifyAccessToken: %v", err) + } + if claims.Username != "admin" { + t.Fatalf("Username = %q, want admin", claims.Username) + } +} From 911e429e56faf776620353dd8146cb200dca8cda Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Tue, 18 Aug 2026 10:35:04 +0330 Subject: [PATCH 3/7] test(CubeOps/internal/auth/jwt_empty_secret_test.go): forge tokens with the empty key and cover the bootstrap guard Signed-off-by: Dwin Gharibi --- .../internal/auth/jwt_empty_secret_test.go | 63 +++++++++++++++-- .../store/jwt_secret_bootstrap_test.go | 69 +++++++++++++++++++ 2 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 CubeOps/internal/store/jwt_secret_bootstrap_test.go diff --git a/CubeOps/internal/auth/jwt_empty_secret_test.go b/CubeOps/internal/auth/jwt_empty_secret_test.go index ff6e052b7..2d7f851e8 100644 --- a/CubeOps/internal/auth/jwt_empty_secret_test.go +++ b/CubeOps/internal/auth/jwt_empty_secret_test.go @@ -7,9 +7,52 @@ import ( "testing" "time" + "github.com/golang-jwt/jwt/v5" "github.com/tencentcloud/CubeSandbox/CubeOps/internal/auth" ) +func mintWithEmptySecret(t *testing.T, claims jwt.Claims) string { + t.Helper() + signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte("")) + if err != nil { + t.Fatalf("failed to mint a token with an empty key: %v", err) + } + return signed +} + +func forgedAccessToken(t *testing.T) string { + t.Helper() + now := time.Now() + return mintWithEmptySecret(t, auth.AccessClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)), + IssuedAt: jwt.NewNumericDate(now), + Subject: "admin", + Audience: jwt.ClaimStrings{"cubeops:access"}, + }, + Username: "admin", + Role: "admin", + Scopes: []string{}, + Typ: "access", + }) +} + +func forgedRefreshToken(t *testing.T) string { + t.Helper() + now := time.Now() + return mintWithEmptySecret(t, auth.RefreshClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(168 * time.Hour)), + IssuedAt: jwt.NewNumericDate(now), + Subject: "admin", + Audience: jwt.ClaimStrings{"cubeops:refresh"}, + }, + Username: "admin", + TokenID: "forged-token-id", + Typ: "refresh", + }) +} + func TestEmptySecretRejectedOnGenerate(t *testing.T) { jm := auth.NewJWTManager("", 15*time.Minute, 168*time.Hour) @@ -21,8 +64,19 @@ func TestEmptySecretRejectedOnGenerate(t *testing.T) { } } -func TestEmptySecretRejectedOnVerify(t *testing.T) { - signer := auth.NewJWTManager("forged-secret-32-bytes-long-ok!!", 15*time.Minute, 168*time.Hour) +func TestForgedTokenSignedWithEmptySecretIsRejected(t *testing.T) { + empty := auth.NewJWTManager("", 15*time.Minute, 168*time.Hour) + + if _, err := empty.VerifyAccessToken(forgedAccessToken(t)); err == nil { + t.Fatal("VerifyAccessToken accepted an admin token forged with the empty signing key") + } + if _, err := empty.VerifyRefreshToken(forgedRefreshToken(t)); err == nil { + t.Fatal("VerifyRefreshToken accepted a token forged with the empty signing key") + } +} + +func TestTokenSignedWithADifferentSecretIsRejected(t *testing.T) { + signer := auth.NewJWTManager("other-secret-32-bytes-long-ok!!!", 15*time.Minute, 168*time.Hour) minted, err := signer.GenerateAccessToken("admin") if err != nil { t.Fatalf("failed to mint a token for the test: %v", err) @@ -30,10 +84,7 @@ func TestEmptySecretRejectedOnVerify(t *testing.T) { empty := auth.NewJWTManager("", 15*time.Minute, 168*time.Hour) if _, err := empty.VerifyAccessToken(minted); err == nil { - t.Fatal("VerifyAccessToken accepted a token while configured with an empty secret") - } - if _, err := empty.VerifyRefreshToken(minted); err == nil { - t.Fatal("VerifyRefreshToken accepted a token while configured with an empty secret") + t.Fatal("VerifyAccessToken accepted a token signed with a different secret") } } diff --git a/CubeOps/internal/store/jwt_secret_bootstrap_test.go b/CubeOps/internal/store/jwt_secret_bootstrap_test.go new file mode 100644 index 000000000..b35617ab5 --- /dev/null +++ b/CubeOps/internal/store/jwt_secret_bootstrap_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2026 Tencent Inc. +// SPDX-License-Identifier: Apache-2.0 + +package store_test + +import ( + "context" + "strings" + "testing" +) + +func TestBootstrapJWTSecretRejectsAnEmptyStoredValue(t *testing.T) { + env := newTestStore(t) + defer env.teardown() + s := env.store + ctx := context.Background() + + if err := s.SetSystemSetting(ctx, "jwt_secret", ""); err != nil { + t.Fatalf("SetSystemSetting: %v", err) + } + + secret, err := s.BootstrapJWTSecret(ctx, "") + if err == nil { + t.Fatalf("BootstrapJWTSecret returned %q with no error for an empty stored secret", secret) + } + if secret != "" { + t.Fatalf("BootstrapJWTSecret returned %q alongside an error, want empty", secret) + } + if !strings.Contains(err.Error(), "empty") { + t.Fatalf("BootstrapJWTSecret error = %v, want it to mention the empty value", err) + } +} + +func TestBootstrapJWTSecretGeneratesAndReusesANonEmptySecret(t *testing.T) { + env := newTestStore(t) + defer env.teardown() + s := env.store + ctx := context.Background() + + first, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) + } + if first == "" { + t.Fatal("BootstrapJWTSecret returned an empty secret on first run") + } + + second, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret (second run): %v", err) + } + if second != first { + t.Fatalf("second run returned a different secret: %q != %q", second, first) + } +} + +func TestBootstrapJWTSecretPrefersTheEnvironmentSecret(t *testing.T) { + env := newTestStore(t) + defer env.teardown() + ctx := context.Background() + + got, err := env.store.BootstrapJWTSecret(ctx, "env-secret-32-bytes-long-enough!") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) + } + if got != "env-secret-32-bytes-long-enough!" { + t.Fatalf("BootstrapJWTSecret = %q, want the env secret", got) + } +} From 0b7eeeabf80e01d035493cb3f3bda4f026ed8eb3 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Tue, 18 Aug 2026 12:33:25 +0330 Subject: [PATCH 4/7] fix(CubeOps/internal/store/db.go): repair an empty JWT secret row and reject whitespace-only secrets Signed-off-by: Dwin Gharibi --- CubeOps/internal/auth/jwt.go | 3 ++- CubeOps/internal/store/db.go | 16 ++++++++++++++-- CubeOps/internal/store/setting.go | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CubeOps/internal/auth/jwt.go b/CubeOps/internal/auth/jwt.go index 352bba50e..0a1636715 100644 --- a/CubeOps/internal/auth/jwt.go +++ b/CubeOps/internal/auth/jwt.go @@ -6,6 +6,7 @@ package auth import ( "errors" "fmt" + "strings" "time" "github.com/golang-jwt/jwt/v5" @@ -62,7 +63,7 @@ func NewJWTManager(secret string, accessTTL, refreshTTL time.Duration) *JWTManag var errEmptyJWTSecret = errors.New("jwt: signing secret is empty") func (m *JWTManager) checkSecret() error { - if len(m.secret) == 0 { + if strings.TrimSpace(string(m.secret)) == "" { return errEmptyJWTSecret } return nil diff --git a/CubeOps/internal/store/db.go b/CubeOps/internal/store/db.go index c2cfb092b..6af0e73c7 100644 --- a/CubeOps/internal/store/db.go +++ b/CubeOps/internal/store/db.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/tencentcloud/CubeSandbox/CubeDB/dao" "github.com/tencentcloud/CubeSandbox/CubeDB/dao/driver/mysql" @@ -141,8 +142,19 @@ func (s *Store) BootstrapJWTSecret(ctx context.Context, envSecret string) (strin if err != nil { return "", fmt.Errorf("persist JWT secret: %w", err) } - if winner == "" { - return "", errors.New("JWT secret resolved to an empty value in t_system_setting") + if strings.TrimSpace(winner) == "" { + if err := s.repairEmptySystemSetting(ctx, "jwt_secret", generated); err != nil { + return "", fmt.Errorf("repair empty JWT secret: %w", err) + } + winner, err = s.GetSystemSetting(ctx, "jwt_secret") + if err != nil { + return "", fmt.Errorf("re-read JWT secret after repair: %w", err) + } + if strings.TrimSpace(winner) == "" { + return "", errors.New("could not resolve a usable JWT secret from t_system_setting: the stored value is empty and the repair did not take, or the read failed; set JWT_SECRET or delete the jwt_secret row") + } + logging.G(ctx).Info("JWT secret in database (t_system_setting) was empty; repaired with a freshly generated value") + return winner, nil } if winner == generated { logging.G(ctx).Info("JWT secret auto-generated and persisted to database (t_system_setting)") diff --git a/CubeOps/internal/store/setting.go b/CubeOps/internal/store/setting.go index bc0306705..43ef9b589 100644 --- a/CubeOps/internal/store/setting.go +++ b/CubeOps/internal/store/setting.go @@ -40,6 +40,13 @@ func (s *Store) GetOrCreateSystemSetting(ctx context.Context, key, value string) return s.GetSystemSetting(ctx, key) } +func (s *Store) repairEmptySystemSetting(ctx context.Context, key, value string) error { + return s.db.WithContext(ctx).Exec( + "UPDATE t_system_setting SET setting_value = ? WHERE setting_key = ? AND (setting_value IS NULL OR setting_value = '')", + value, key, + ).Error +} + // SetSystemSetting upserts a system-level setting value. func (s *Store) SetSystemSetting(ctx context.Context, key, value string) error { return s.db.WithContext(ctx).Exec( From efbbffde291ceddf46c5476d15dd861317902916 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Tue, 18 Aug 2026 12:33:25 +0330 Subject: [PATCH 5/7] test(CubeOps/internal/store/jwt_secret_bootstrap_test.go): cover repair, convergence and whitespace secrets Signed-off-by: Dwin Gharibi --- .../internal/auth/jwt_empty_secret_test.go | 18 ++++---- .../store/jwt_secret_bootstrap_test.go | 46 ++++++++++++++++--- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/CubeOps/internal/auth/jwt_empty_secret_test.go b/CubeOps/internal/auth/jwt_empty_secret_test.go index 2d7f851e8..ffd9057ca 100644 --- a/CubeOps/internal/auth/jwt_empty_secret_test.go +++ b/CubeOps/internal/auth/jwt_empty_secret_test.go @@ -75,16 +75,16 @@ func TestForgedTokenSignedWithEmptySecretIsRejected(t *testing.T) { } } -func TestTokenSignedWithADifferentSecretIsRejected(t *testing.T) { - signer := auth.NewJWTManager("other-secret-32-bytes-long-ok!!!", 15*time.Minute, 168*time.Hour) - minted, err := signer.GenerateAccessToken("admin") - if err != nil { - t.Fatalf("failed to mint a token for the test: %v", err) - } +func TestWhitespaceOnlySecretIsTreatedAsEmpty(t *testing.T) { + for _, secret := range []string{" ", "\t", "\n", " \t\n "} { + jm := auth.NewJWTManager(secret, 15*time.Minute, 168*time.Hour) - empty := auth.NewJWTManager("", 15*time.Minute, 168*time.Hour) - if _, err := empty.VerifyAccessToken(minted); err == nil { - t.Fatal("VerifyAccessToken accepted a token signed with a different secret") + if _, err := jm.GenerateAccessToken("admin"); err == nil { + t.Errorf("GenerateAccessToken accepted a whitespace-only secret %q", secret) + } + if _, err := jm.VerifyAccessToken(forgedAccessToken(t)); err == nil { + t.Errorf("VerifyAccessToken accepted a forged token with a whitespace-only secret %q", secret) + } } } diff --git a/CubeOps/internal/store/jwt_secret_bootstrap_test.go b/CubeOps/internal/store/jwt_secret_bootstrap_test.go index b35617ab5..df0baeb70 100644 --- a/CubeOps/internal/store/jwt_secret_bootstrap_test.go +++ b/CubeOps/internal/store/jwt_secret_bootstrap_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestBootstrapJWTSecretRejectsAnEmptyStoredValue(t *testing.T) { +func TestBootstrapJWTSecretRepairsAnEmptyStoredValue(t *testing.T) { env := newTestStore(t) defer env.teardown() s := env.store @@ -20,14 +20,46 @@ func TestBootstrapJWTSecretRejectsAnEmptyStoredValue(t *testing.T) { } secret, err := s.BootstrapJWTSecret(ctx, "") - if err == nil { - t.Fatalf("BootstrapJWTSecret returned %q with no error for an empty stored secret", secret) + if err != nil { + t.Fatalf("BootstrapJWTSecret did not repair an empty stored secret: %v", err) + } + if strings.TrimSpace(secret) == "" { + t.Fatal("BootstrapJWTSecret returned an empty secret after repair") + } + + persisted, err := s.GetSystemSetting(ctx, "jwt_secret") + if err != nil { + t.Fatalf("GetSystemSetting: %v", err) + } + if persisted != secret { + t.Fatalf("repaired secret was not persisted: row=%q returned=%q", persisted, secret) } - if secret != "" { - t.Fatalf("BootstrapJWTSecret returned %q alongside an error, want empty", secret) + + again, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret after repair: %v", err) + } + if again != secret { + t.Fatalf("a later start got a different secret: %q != %q", again, secret) + } +} + +func TestBootstrapJWTSecretDoesNotOverwriteAHealthyValue(t *testing.T) { + env := newTestStore(t) + defer env.teardown() + s := env.store + ctx := context.Background() + + if err := s.SetSystemSetting(ctx, "jwt_secret", "already-good-32-bytes-long-ok!!!"); err != nil { + t.Fatalf("SetSystemSetting: %v", err) + } + + secret, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) } - if !strings.Contains(err.Error(), "empty") { - t.Fatalf("BootstrapJWTSecret error = %v, want it to mention the empty value", err) + if secret != "already-good-32-bytes-long-ok!!!" { + t.Fatalf("BootstrapJWTSecret = %q, want the stored value untouched", secret) } } From 11a739baeb34b9ed873078a48e28712927e98775 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Wed, 19 Aug 2026 11:38:58 +0330 Subject: [PATCH 6/7] fix(CubeOps/internal/store/setting.go): repair any degenerate jwt_secret value and ignore a blank JWT_SECRET Signed-off-by: Dwin Gharibi --- CubeOps/internal/store/db.go | 4 ++-- CubeOps/internal/store/setting.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CubeOps/internal/store/db.go b/CubeOps/internal/store/db.go index 6af0e73c7..ac8bd6d6a 100644 --- a/CubeOps/internal/store/db.go +++ b/CubeOps/internal/store/db.go @@ -128,7 +128,7 @@ func (s *Store) bootstrapMasterKey(ctx context.Context) error { // settings table, and returned. This allows zero-config deployment — the // secret is auto-generated on first run and reused on subsequent runs. func (s *Store) BootstrapJWTSecret(ctx context.Context, envSecret string) (string, error) { - if envSecret != "" { + if strings.TrimSpace(envSecret) != "" { return envSecret, nil } // Use GetOrCreateSystemSetting (INSERT IGNORE + read-back) so that @@ -143,7 +143,7 @@ func (s *Store) BootstrapJWTSecret(ctx context.Context, envSecret string) (strin return "", fmt.Errorf("persist JWT secret: %w", err) } if strings.TrimSpace(winner) == "" { - if err := s.repairEmptySystemSetting(ctx, "jwt_secret", generated); err != nil { + if err := s.repairSystemSettingIfStill(ctx, "jwt_secret", winner, generated); err != nil { return "", fmt.Errorf("repair empty JWT secret: %w", err) } winner, err = s.GetSystemSetting(ctx, "jwt_secret") diff --git a/CubeOps/internal/store/setting.go b/CubeOps/internal/store/setting.go index 43ef9b589..ca188cfcd 100644 --- a/CubeOps/internal/store/setting.go +++ b/CubeOps/internal/store/setting.go @@ -40,10 +40,10 @@ func (s *Store) GetOrCreateSystemSetting(ctx context.Context, key, value string) return s.GetSystemSetting(ctx, key) } -func (s *Store) repairEmptySystemSetting(ctx context.Context, key, value string) error { +func (s *Store) repairSystemSettingIfStill(ctx context.Context, key, observed, value string) error { return s.db.WithContext(ctx).Exec( - "UPDATE t_system_setting SET setting_value = ? WHERE setting_key = ? AND (setting_value IS NULL OR setting_value = '')", - value, key, + "UPDATE t_system_setting SET setting_value = ? WHERE setting_key = ? AND (setting_value IS NULL OR setting_value = ?)", + value, key, observed, ).Error } From f02b4b02e6d708ad8399dfaa7836fbe5d72292b6 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Wed, 19 Aug 2026 11:38:59 +0330 Subject: [PATCH 7/7] test(CubeOps/internal/store/jwt_secret_bootstrap_test.go): cover blank, tab, newline and NULL stored secrets Signed-off-by: Dwin Gharibi --- .../store/jwt_secret_bootstrap_test.go | 165 ++++++++++-------- 1 file changed, 94 insertions(+), 71 deletions(-) diff --git a/CubeOps/internal/store/jwt_secret_bootstrap_test.go b/CubeOps/internal/store/jwt_secret_bootstrap_test.go index df0baeb70..ca1f556e9 100644 --- a/CubeOps/internal/store/jwt_secret_bootstrap_test.go +++ b/CubeOps/internal/store/jwt_secret_bootstrap_test.go @@ -5,97 +5,120 @@ package store_test import ( "context" + "database/sql" "strings" "testing" ) -func TestBootstrapJWTSecretRepairsAnEmptyStoredValue(t *testing.T) { +func TestBootstrapJWTSecret(t *testing.T) { env := newTestStore(t) defer env.teardown() s := env.store ctx := context.Background() - if err := s.SetSystemSetting(ctx, "jwt_secret", ""); err != nil { - t.Fatalf("SetSystemSetting: %v", err) - } - - secret, err := s.BootstrapJWTSecret(ctx, "") + raw, err := sql.Open("mysql", env.dsn) if err != nil { - t.Fatalf("BootstrapJWTSecret did not repair an empty stored secret: %v", err) - } - if strings.TrimSpace(secret) == "" { - t.Fatal("BootstrapJWTSecret returned an empty secret after repair") + t.Fatalf("open raw connection: %v", err) } + defer raw.Close() - persisted, err := s.GetSystemSetting(ctx, "jwt_secret") - if err != nil { - t.Fatalf("GetSystemSetting: %v", err) - } - if persisted != secret { - t.Fatalf("repaired secret was not persisted: row=%q returned=%q", persisted, secret) + setRow := func(t *testing.T, value any) { + t.Helper() + if _, err := raw.ExecContext(ctx, + "REPLACE INTO t_system_setting (setting_key, setting_value) VALUES (?, ?)", + "jwt_secret", value); err != nil { + t.Fatalf("seed jwt_secret=%v: %v", value, err) + } + } + clearRow := func(t *testing.T) { + t.Helper() + if _, err := raw.ExecContext(ctx, + "DELETE FROM t_system_setting WHERE setting_key = ?", "jwt_secret"); err != nil { + t.Fatalf("delete jwt_secret: %v", err) + } + } + readRow := func(t *testing.T) string { + t.Helper() + var v sql.NullString + if err := raw.QueryRowContext(ctx, + "SELECT setting_value FROM t_system_setting WHERE setting_key = ?", "jwt_secret").Scan(&v); err != nil { + t.Fatalf("read jwt_secret: %v", err) + } + return v.String } - again, err := s.BootstrapJWTSecret(ctx, "") - if err != nil { - t.Fatalf("BootstrapJWTSecret after repair: %v", err) - } - if again != secret { - t.Fatalf("a later start got a different secret: %q != %q", again, secret) - } -} + t.Run("generates and reuses on a clean database", func(t *testing.T) { + clearRow(t) -func TestBootstrapJWTSecretDoesNotOverwriteAHealthyValue(t *testing.T) { - env := newTestStore(t) - defer env.teardown() - s := env.store - ctx := context.Background() + first, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) + } + if first == "" { + t.Fatal("returned an empty secret on first run") + } + second, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret (second run): %v", err) + } + if second != first { + t.Fatalf("a later start got a different secret: %q != %q", second, first) + } + }) - if err := s.SetSystemSetting(ctx, "jwt_secret", "already-good-32-bytes-long-ok!!!"); err != nil { - t.Fatalf("SetSystemSetting: %v", err) - } + t.Run("repairs degenerate stored values", func(t *testing.T) { + for _, stored := range []any{"", " ", "\t", " \t ", "\n", nil} { + setRow(t, stored) - secret, err := s.BootstrapJWTSecret(ctx, "") - if err != nil { - t.Fatalf("BootstrapJWTSecret: %v", err) - } - if secret != "already-good-32-bytes-long-ok!!!" { - t.Fatalf("BootstrapJWTSecret = %q, want the stored value untouched", secret) - } -} + secret, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Errorf("did not repair stored value %#v: %v", stored, err) + continue + } + if strings.TrimSpace(secret) == "" { + t.Errorf("returned %q after repairing %#v", secret, stored) + continue + } + if got := readRow(t); got != secret { + t.Errorf("repair for %#v was not persisted: row=%q returned=%q", stored, got, secret) + } + } + }) -func TestBootstrapJWTSecretGeneratesAndReusesANonEmptySecret(t *testing.T) { - env := newTestStore(t) - defer env.teardown() - s := env.store - ctx := context.Background() + t.Run("does not overwrite a healthy value", func(t *testing.T) { + setRow(t, "already-good-32-bytes-long-ok!!!") - first, err := s.BootstrapJWTSecret(ctx, "") - if err != nil { - t.Fatalf("BootstrapJWTSecret: %v", err) - } - if first == "" { - t.Fatal("BootstrapJWTSecret returned an empty secret on first run") - } + secret, err := s.BootstrapJWTSecret(ctx, "") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) + } + if secret != "already-good-32-bytes-long-ok!!!" { + t.Fatalf("BootstrapJWTSecret = %q, want the stored value untouched", secret) + } + }) - second, err := s.BootstrapJWTSecret(ctx, "") - if err != nil { - t.Fatalf("BootstrapJWTSecret (second run): %v", err) - } - if second != first { - t.Fatalf("second run returned a different secret: %q != %q", second, first) - } -} + t.Run("prefers a usable environment secret", func(t *testing.T) { + setRow(t, "db-secret-32-bytes-long-enough!!") -func TestBootstrapJWTSecretPrefersTheEnvironmentSecret(t *testing.T) { - env := newTestStore(t) - defer env.teardown() - ctx := context.Background() + secret, err := s.BootstrapJWTSecret(ctx, "env-secret-32-bytes-long-enough!") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) + } + if secret != "env-secret-32-bytes-long-enough!" { + t.Fatalf("BootstrapJWTSecret = %q, want the env secret", secret) + } + }) - got, err := env.store.BootstrapJWTSecret(ctx, "env-secret-32-bytes-long-enough!") - if err != nil { - t.Fatalf("BootstrapJWTSecret: %v", err) - } - if got != "env-secret-32-bytes-long-enough!" { - t.Fatalf("BootstrapJWTSecret = %q, want the env secret", got) - } + t.Run("ignores a whitespace-only environment secret", func(t *testing.T) { + setRow(t, "db-secret-32-bytes-long-enough!!") + + secret, err := s.BootstrapJWTSecret(ctx, " ") + if err != nil { + t.Fatalf("BootstrapJWTSecret: %v", err) + } + if secret != "db-secret-32-bytes-long-enough!!" { + t.Fatalf("BootstrapJWTSecret = %q, want the stored secret; a whitespace-only "+ + "JWT_SECRET would make every token operation fail at runtime", secret) + } + }) }