Files
gitea-tea/modules/credstore/encrypted_store_test.go
T
Bo-Yi Wu f6d939a8df refactor(credstore): embed credential store and drop sdk-go dependency
- Embed the minimal credstore subset used by tea (SecureStore,
  EncryptedFileStore, KeyringStore, FileStore) as modules/credstore so
  external SDK renames can no longer break the build
- Keep the on-disk format fully compatible: AES-256-GCM values with the
  v1: prefix, credentials.json / credentials.json.enc paths, and the
  Token JSON field names are unchanged, verified by a ciphertext fixture
  generated with sdk-go v1.1.0
- Store the keyring master key under a tea-owned account name
- Reuse the existing kernel-level filelock module instead of the
  upstream lockfile protocol, removing a stale-lock race
- Cover roundtrip, keyring-unavailable fallback, and fixture decryption
  with tests using a mocked keyring
- Remove github.com/go-signet/sdk-go and promote
  github.com/zalando/go-keyring to a direct dependency

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:59:07 +08:00

141 lines
4.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package credstore
import (
"encoding/base64"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zalando/go-keyring"
)
// newLargeTestToken builds a token whose access token is several KB,
// mimicking real JWTs with large groups claims that exceed the Windows
// Credential Manager 2560-byte blob limit.
func newLargeTestToken(clientID string) Token {
return Token{
AccessToken: "header." + strings.Repeat("groups-claim-payload-", 300) + ".sig",
RefreshToken: "test-refresh-token",
TokenType: "Bearer",
ExpiresAt: time.Now().Add(1 * time.Hour).Truncate(time.Second),
ClientID: clientID,
}
}
func newTestEncryptedStore(t *testing.T) (*EncryptedFileStore[Token], string) {
t.Helper()
keyring.MockInit()
path := filepath.Join(t.TempDir(), "tokens.enc")
return NewEncryptedFileStore[Token]("test-service", path, JSONCodec[Token]{}), path
}
func TestEncryptedFileStoreSaveAndLoad(t *testing.T) {
store, _ := newTestEncryptedStore(t)
tok := newLargeTestToken("test-client")
require.NoError(t, store.Save(tok.ClientID, tok))
loaded, err := store.Load("test-client")
require.NoError(t, err)
assert.Equal(t, tok.AccessToken, loaded.AccessToken)
assert.Equal(t, tok.RefreshToken, loaded.RefreshToken)
assert.True(t, tok.ExpiresAt.Equal(loaded.ExpiresAt))
}
func TestEncryptedFileStoreFileContainsNoPlaintext(t *testing.T) {
store, path := newTestEncryptedStore(t)
tok := newLargeTestToken("test-client")
require.NoError(t, store.Save(tok.ClientID, tok))
raw, err := os.ReadFile(path)
require.NoError(t, err)
assert.NotContains(t, string(raw), "groups-claim-payload")
assert.NotContains(t, string(raw), tok.RefreshToken)
}
func TestEncryptedFileStoreKeyringHoldsOnlySmallMasterKey(t *testing.T) {
store, _ := newTestEncryptedStore(t)
tok := newLargeTestToken("test-client")
require.NoError(t, store.Save(tok.ClientID, tok))
// The token itself must not be in the keyring.
_, err := keyring.Get("test-service", "test-client")
assert.ErrorIs(t, err, keyring.ErrNotFound)
// Only the 44-byte base64 master key may live in the keyring —
// well under the Windows Credential Manager 2560-byte blob limit.
encoded, err := keyring.Get("test-service", masterKeyUser)
require.NoError(t, err)
assert.Len(t, encoded, 44)
key, err := base64.StdEncoding.DecodeString(encoded)
require.NoError(t, err)
assert.Len(t, key, 32)
}
func TestEncryptedFileStoreLoadNotFound(t *testing.T) {
store, _ := newTestEncryptedStore(t)
_, err := store.Load("nonexistent")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestEncryptedFileStoreDelete(t *testing.T) {
store, _ := newTestEncryptedStore(t)
tok := newLargeTestToken("test-client")
require.NoError(t, store.Save(tok.ClientID, tok))
require.NoError(t, store.Delete("test-client"))
_, err := store.Load("test-client")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestEncryptedFileStoreSaveEmptyClientID(t *testing.T) {
store, _ := newTestEncryptedStore(t)
err := store.Save("", newLargeTestToken("x"))
assert.ErrorIs(t, err, ErrEmptyClientID)
}
func TestEncryptedFileStoreProbe(t *testing.T) {
store, _ := newTestEncryptedStore(t)
assert.True(t, store.Probe())
}
func TestEncryptedFileStoreCorruptMasterKeyFailsLoad(t *testing.T) {
store, _ := newTestEncryptedStore(t)
tok := newLargeTestToken("test-client")
require.NoError(t, store.Save(tok.ClientID, tok))
// A fresh store whose keyring holds a corrupted key must fail to decrypt
// rather than return garbage or mint a new key.
require.NoError(t, keyring.Set("test-service", masterKeyUser, "not-base64!"))
fresh := NewEncryptedFileStore[Token]("test-service", store.file.filePath, JSONCodec[Token]{})
assert.False(t, fresh.Probe())
_, err := fresh.Load("test-client")
require.Error(t, err)
assert.NotErrorIs(t, err, ErrNotFound)
}
func TestEncryptedFileStoreNilCodecPanics(t *testing.T) {
assert.Panics(t, func() {
NewEncryptedFileStore[string]("svc", "path", nil)
})
}
func TestEncryptedFileStoreString(t *testing.T) {
store, path := newTestEncryptedStore(t)
assert.Equal(t, "encrypted-file: "+path, store.String())
}