security_secret_bootstrap_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/hex"
  6. "errors"
  7. "fmt"
  8. "strings"
  9. "sync"
  10. "testing"
  11. dbent "github.com/Wei-Shaw/sub2api/ent"
  12. "github.com/Wei-Shaw/sub2api/ent/enttest"
  13. "github.com/Wei-Shaw/sub2api/ent/securitysecret"
  14. "github.com/Wei-Shaw/sub2api/internal/config"
  15. "github.com/stretchr/testify/require"
  16. "entgo.io/ent/dialect"
  17. entsql "entgo.io/ent/dialect/sql"
  18. _ "modernc.org/sqlite"
  19. )
  20. func newSecuritySecretTestClient(t *testing.T) *dbent.Client {
  21. t.Helper()
  22. name := strings.ReplaceAll(t.Name(), "/", "_")
  23. dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared&_fk=1", name)
  24. db, err := sql.Open("sqlite", dsn)
  25. require.NoError(t, err)
  26. t.Cleanup(func() { _ = db.Close() })
  27. _, err = db.Exec("PRAGMA foreign_keys = ON")
  28. require.NoError(t, err)
  29. drv := entsql.OpenDB(dialect.SQLite, db)
  30. client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
  31. t.Cleanup(func() { _ = client.Close() })
  32. return client
  33. }
  34. func TestEnsureBootstrapSecretsNilInputs(t *testing.T) {
  35. err := ensureBootstrapSecrets(context.Background(), nil, &config.Config{})
  36. require.Error(t, err)
  37. require.Contains(t, err.Error(), "nil ent client")
  38. client := newSecuritySecretTestClient(t)
  39. err = ensureBootstrapSecrets(context.Background(), client, nil)
  40. require.Error(t, err)
  41. require.Contains(t, err.Error(), "nil config")
  42. }
  43. func TestEnsureBootstrapSecretsGenerateAndPersistJWTSecret(t *testing.T) {
  44. client := newSecuritySecretTestClient(t)
  45. cfg := &config.Config{}
  46. err := ensureBootstrapSecrets(context.Background(), client, cfg)
  47. require.NoError(t, err)
  48. require.NotEmpty(t, cfg.JWT.Secret)
  49. require.GreaterOrEqual(t, len([]byte(cfg.JWT.Secret)), 32)
  50. stored, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(securitySecretKeyJWT)).Only(context.Background())
  51. require.NoError(t, err)
  52. require.Equal(t, cfg.JWT.Secret, stored.Value)
  53. }
  54. func TestEnsureBootstrapSecretsLoadExistingJWTSecret(t *testing.T) {
  55. client := newSecuritySecretTestClient(t)
  56. _, err := client.SecuritySecret.Create().SetKey(securitySecretKeyJWT).SetValue("existing-jwt-secret-32bytes-long!!!!").Save(context.Background())
  57. require.NoError(t, err)
  58. cfg := &config.Config{}
  59. err = ensureBootstrapSecrets(context.Background(), client, cfg)
  60. require.NoError(t, err)
  61. require.Equal(t, "existing-jwt-secret-32bytes-long!!!!", cfg.JWT.Secret)
  62. }
  63. func TestEnsureBootstrapSecretsRejectInvalidStoredSecret(t *testing.T) {
  64. client := newSecuritySecretTestClient(t)
  65. _, err := client.SecuritySecret.Create().SetKey(securitySecretKeyJWT).SetValue("too-short").Save(context.Background())
  66. require.NoError(t, err)
  67. cfg := &config.Config{}
  68. err = ensureBootstrapSecrets(context.Background(), client, cfg)
  69. require.Error(t, err)
  70. require.Contains(t, err.Error(), "at least 32 bytes")
  71. }
  72. func TestEnsureBootstrapSecretsPersistConfiguredJWTSecret(t *testing.T) {
  73. client := newSecuritySecretTestClient(t)
  74. cfg := &config.Config{
  75. JWT: config.JWTConfig{Secret: "configured-jwt-secret-32bytes-long!!"},
  76. }
  77. err := ensureBootstrapSecrets(context.Background(), client, cfg)
  78. require.NoError(t, err)
  79. stored, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(securitySecretKeyJWT)).Only(context.Background())
  80. require.NoError(t, err)
  81. require.Equal(t, "configured-jwt-secret-32bytes-long!!", stored.Value)
  82. }
  83. func TestEnsureBootstrapSecretsConfiguredSecretTooShort(t *testing.T) {
  84. client := newSecuritySecretTestClient(t)
  85. cfg := &config.Config{JWT: config.JWTConfig{Secret: "short"}}
  86. err := ensureBootstrapSecrets(context.Background(), client, cfg)
  87. require.Error(t, err)
  88. require.Contains(t, err.Error(), "at least 32 bytes")
  89. }
  90. func TestEnsureBootstrapSecretsConfiguredSecretDuplicateIgnored(t *testing.T) {
  91. client := newSecuritySecretTestClient(t)
  92. _, err := client.SecuritySecret.Create().
  93. SetKey(securitySecretKeyJWT).
  94. SetValue("existing-jwt-secret-32bytes-long!!!!").
  95. Save(context.Background())
  96. require.NoError(t, err)
  97. cfg := &config.Config{JWT: config.JWTConfig{Secret: "another-configured-jwt-secret-32!!!!"}}
  98. err = ensureBootstrapSecrets(context.Background(), client, cfg)
  99. require.NoError(t, err)
  100. stored, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(securitySecretKeyJWT)).Only(context.Background())
  101. require.NoError(t, err)
  102. require.Equal(t, "existing-jwt-secret-32bytes-long!!!!", stored.Value)
  103. require.Equal(t, "existing-jwt-secret-32bytes-long!!!!", cfg.JWT.Secret)
  104. }
  105. func TestGetOrCreateGeneratedSecuritySecretTrimmedExistingValue(t *testing.T) {
  106. client := newSecuritySecretTestClient(t)
  107. _, err := client.SecuritySecret.Create().
  108. SetKey("trimmed_key").
  109. SetValue(" existing-trimmed-secret-32bytes-long!! ").
  110. Save(context.Background())
  111. require.NoError(t, err)
  112. value, created, err := getOrCreateGeneratedSecuritySecret(context.Background(), client, "trimmed_key", 32)
  113. require.NoError(t, err)
  114. require.False(t, created)
  115. require.Equal(t, "existing-trimmed-secret-32bytes-long!!", value)
  116. }
  117. func TestGetOrCreateGeneratedSecuritySecretQueryError(t *testing.T) {
  118. client := newSecuritySecretTestClient(t)
  119. require.NoError(t, client.Close())
  120. _, _, err := getOrCreateGeneratedSecuritySecret(context.Background(), client, "closed_client_key", 32)
  121. require.Error(t, err)
  122. }
  123. func TestGetOrCreateGeneratedSecuritySecretCreateValidationError(t *testing.T) {
  124. client := newSecuritySecretTestClient(t)
  125. tooLongKey := strings.Repeat("k", 101)
  126. _, _, err := getOrCreateGeneratedSecuritySecret(context.Background(), client, tooLongKey, 32)
  127. require.Error(t, err)
  128. }
  129. func TestGetOrCreateGeneratedSecuritySecretConcurrentCreation(t *testing.T) {
  130. client := newSecuritySecretTestClient(t)
  131. const goroutines = 8
  132. key := "concurrent_bootstrap_key"
  133. values := make([]string, goroutines)
  134. createdFlags := make([]bool, goroutines)
  135. errs := make([]error, goroutines)
  136. var wg sync.WaitGroup
  137. for i := 0; i < goroutines; i++ {
  138. wg.Add(1)
  139. go func(idx int) {
  140. defer wg.Done()
  141. values[idx], createdFlags[idx], errs[idx] = getOrCreateGeneratedSecuritySecret(context.Background(), client, key, 32)
  142. }(i)
  143. }
  144. wg.Wait()
  145. for i := range errs {
  146. require.NoError(t, errs[i])
  147. require.NotEmpty(t, values[i])
  148. }
  149. for i := 1; i < len(values); i++ {
  150. require.Equal(t, values[0], values[i])
  151. }
  152. createdCount := 0
  153. for _, created := range createdFlags {
  154. if created {
  155. createdCount++
  156. }
  157. }
  158. require.GreaterOrEqual(t, createdCount, 1)
  159. require.LessOrEqual(t, createdCount, 1)
  160. count, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(key)).Count(context.Background())
  161. require.NoError(t, err)
  162. require.Equal(t, 1, count)
  163. }
  164. func TestGetOrCreateGeneratedSecuritySecretGenerateError(t *testing.T) {
  165. client := newSecuritySecretTestClient(t)
  166. originalRead := readRandomBytes
  167. readRandomBytes = func([]byte) (int, error) {
  168. return 0, errors.New("boom")
  169. }
  170. t.Cleanup(func() {
  171. readRandomBytes = originalRead
  172. })
  173. _, _, err := getOrCreateGeneratedSecuritySecret(context.Background(), client, "gen_error_key", 32)
  174. require.Error(t, err)
  175. require.Contains(t, err.Error(), "boom")
  176. }
  177. func TestCreateSecuritySecretIfAbsent(t *testing.T) {
  178. client := newSecuritySecretTestClient(t)
  179. _, err := createSecuritySecretIfAbsent(context.Background(), client, "abc", "short")
  180. require.Error(t, err)
  181. require.Contains(t, err.Error(), "at least 32 bytes")
  182. stored, err := createSecuritySecretIfAbsent(context.Background(), client, "abc", "valid-jwt-secret-value-32bytes-long")
  183. require.NoError(t, err)
  184. require.Equal(t, "valid-jwt-secret-value-32bytes-long", stored)
  185. stored, err = createSecuritySecretIfAbsent(context.Background(), client, "abc", "another-valid-secret-value-32bytes")
  186. require.NoError(t, err)
  187. require.Equal(t, "valid-jwt-secret-value-32bytes-long", stored)
  188. count, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ("abc")).Count(context.Background())
  189. require.NoError(t, err)
  190. require.Equal(t, 1, count)
  191. }
  192. func TestCreateSecuritySecretIfAbsentValidationError(t *testing.T) {
  193. client := newSecuritySecretTestClient(t)
  194. _, err := createSecuritySecretIfAbsent(
  195. context.Background(),
  196. client,
  197. strings.Repeat("k", 101),
  198. "valid-jwt-secret-value-32bytes-long",
  199. )
  200. require.Error(t, err)
  201. }
  202. func TestCreateSecuritySecretIfAbsentExecError(t *testing.T) {
  203. client := newSecuritySecretTestClient(t)
  204. require.NoError(t, client.Close())
  205. _, err := createSecuritySecretIfAbsent(context.Background(), client, "closed-client-key", "valid-jwt-secret-value-32bytes-long")
  206. require.Error(t, err)
  207. }
  208. func TestQuerySecuritySecretWithRetrySuccess(t *testing.T) {
  209. client := newSecuritySecretTestClient(t)
  210. created, err := client.SecuritySecret.Create().
  211. SetKey("retry_success_key").
  212. SetValue("retry-success-jwt-secret-value-32!!").
  213. Save(context.Background())
  214. require.NoError(t, err)
  215. got, err := querySecuritySecretWithRetry(context.Background(), client, "retry_success_key")
  216. require.NoError(t, err)
  217. require.Equal(t, created.ID, got.ID)
  218. require.Equal(t, "retry-success-jwt-secret-value-32!!", got.Value)
  219. }
  220. func TestQuerySecuritySecretWithRetryExhausted(t *testing.T) {
  221. client := newSecuritySecretTestClient(t)
  222. _, err := querySecuritySecretWithRetry(context.Background(), client, "retry_missing_key")
  223. require.Error(t, err)
  224. require.True(t, isSecretNotFoundError(err))
  225. }
  226. func TestQuerySecuritySecretWithRetryContextCanceled(t *testing.T) {
  227. client := newSecuritySecretTestClient(t)
  228. ctx, cancel := context.WithTimeout(context.Background(), securitySecretReadRetryWait/2)
  229. defer cancel()
  230. _, err := querySecuritySecretWithRetry(ctx, client, "retry_ctx_cancel_key")
  231. require.Error(t, err)
  232. require.ErrorIs(t, err, context.DeadlineExceeded)
  233. }
  234. func TestQuerySecuritySecretWithRetryNonNotFoundError(t *testing.T) {
  235. client := newSecuritySecretTestClient(t)
  236. require.NoError(t, client.Close())
  237. _, err := querySecuritySecretWithRetry(context.Background(), client, "retry_closed_client_key")
  238. require.Error(t, err)
  239. require.False(t, isSecretNotFoundError(err))
  240. }
  241. func TestSecretNotFoundHelpers(t *testing.T) {
  242. require.False(t, isSecretNotFoundError(nil))
  243. require.False(t, isSQLNoRowsError(nil))
  244. require.True(t, isSQLNoRowsError(sql.ErrNoRows))
  245. require.True(t, isSQLNoRowsError(fmt.Errorf("wrapped: %w", sql.ErrNoRows)))
  246. require.True(t, isSQLNoRowsError(errors.New("sql: no rows in result set")))
  247. require.True(t, isSecretNotFoundError(sql.ErrNoRows))
  248. require.True(t, isSecretNotFoundError(errors.New("sql: no rows in result set")))
  249. require.False(t, isSecretNotFoundError(errors.New("some other error")))
  250. }
  251. func TestGenerateHexSecretReadError(t *testing.T) {
  252. originalRead := readRandomBytes
  253. readRandomBytes = func([]byte) (int, error) {
  254. return 0, errors.New("read random failed")
  255. }
  256. t.Cleanup(func() {
  257. readRandomBytes = originalRead
  258. })
  259. _, err := generateHexSecret(32)
  260. require.Error(t, err)
  261. require.Contains(t, err.Error(), "read random failed")
  262. }
  263. func TestGenerateHexSecretLengths(t *testing.T) {
  264. v1, err := generateHexSecret(0)
  265. require.NoError(t, err)
  266. require.Len(t, v1, 64)
  267. _, err = hex.DecodeString(v1)
  268. require.NoError(t, err)
  269. v2, err := generateHexSecret(16)
  270. require.NoError(t, err)
  271. require.Len(t, v2, 32)
  272. _, err = hex.DecodeString(v2)
  273. require.NoError(t, err)
  274. require.NotEqual(t, v1, v2)
  275. }