Files
gitea-tea/modules/credstore/file_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

221 lines
6.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package credstore
import (
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestFileStore(t *testing.T) (*FileStore[Token], string) {
t.Helper()
path := filepath.Join(t.TempDir(), "tokens.json")
return NewFileStore[Token](path, JSONCodec[Token]{}), path
}
func TestFileStoreSaveAndLoad(t *testing.T) {
store, _ := newTestFileStore(t)
tok := Token{
AccessToken: "test-access-token",
RefreshToken: "test-refresh-token",
TokenType: "Bearer",
ExpiresAt: time.Now().Add(1 * time.Hour).Truncate(time.Second),
ClientID: "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.Equal(t, tok.ClientID, loaded.ClientID)
}
func TestFileStoreLoadNotFound(t *testing.T) {
store, _ := newTestFileStore(t)
_, err := store.Load("nonexistent")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestFileStoreLoadFromExistingFileNotFound(t *testing.T) {
store, _ := newTestFileStore(t)
require.NoError(t, store.Save("client-1", Token{AccessToken: "token-1", ClientID: "client-1"}))
_, err := store.Load("client-2")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestFileStoreDelete(t *testing.T) {
store, _ := newTestFileStore(t)
require.NoError(t, store.Save("test-client", Token{AccessToken: "test-token", ClientID: "test-client"}))
require.NoError(t, store.Delete("test-client"))
_, err := store.Load("test-client")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestFileStoreDeleteNonexistent(t *testing.T) {
store, _ := newTestFileStore(t)
// Should not error when deleting from nonexistent file
assert.NoError(t, store.Delete("nonexistent"))
}
func TestFileStoreDeletePreservesOtherClients(t *testing.T) {
store, _ := newTestFileStore(t)
for _, id := range []string{"client-1", "client-2"} {
require.NoError(t, store.Save(id, Token{AccessToken: "token-" + id, ClientID: id}))
}
require.NoError(t, store.Delete("client-1"))
loaded, err := store.Load("client-2")
require.NoError(t, err)
assert.Equal(t, "token-client-2", loaded.AccessToken)
}
func TestFileStoreConcurrentWrites(t *testing.T) {
store, _ := newTestFileStore(t)
const goroutines = 10
var wg sync.WaitGroup
wg.Add(goroutines)
for i := range goroutines {
go func(id int) {
defer wg.Done()
tok := Token{
AccessToken: fmt.Sprintf("access-token-%d", id),
ClientID: fmt.Sprintf("client-%d", id),
}
assert.NoError(t, store.Save(tok.ClientID, tok))
}(i)
}
wg.Wait()
// Verify all tokens were saved by loading each one
for i := range goroutines {
clientID := fmt.Sprintf("client-%d", i)
loaded, err := store.Load(clientID)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("access-token-%d", i), loaded.AccessToken)
}
// The kernel lock is released after the saves: the lock file (which
// legitimately remains on disk with flock-style locking) must be
// immediately re-lockable without hitting the timeout.
require.NoError(t, store.withFileLock(func() error { return nil }))
}
func TestFileStoreSaveEmptyClientID(t *testing.T) {
store, _ := newTestFileStore(t)
err := store.Save("", Token{AccessToken: "tok"})
assert.ErrorIs(t, err, ErrEmptyClientID)
}
func TestFileStoreFilePermissions(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("file permission test is not applicable on Windows")
}
store, path := newTestFileStore(t)
require.NoError(t, store.Save("c1", Token{AccessToken: "tok", ClientID: "c1"}))
info, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}
func TestFileStoreSaveCreatesParentDirectories(t *testing.T) {
nestedPath := filepath.Join(t.TempDir(), "a", "b", "c", "tokens.json")
store := NewFileStore[Token](nestedPath, JSONCodec[Token]{})
require.NoError(t, store.Save("c1", Token{AccessToken: "tok", ClientID: "c1"}))
loaded, err := store.Load("c1")
require.NoError(t, err)
assert.Equal(t, "tok", loaded.AccessToken)
}
func TestFileStoreInvalidJSON(t *testing.T) {
store, path := newTestFileStore(t)
require.NoError(t, os.WriteFile(path, []byte("{invalid"), 0o600))
_, err := store.Load("any")
assert.Error(t, err)
}
func TestFileStoreNullDataField(t *testing.T) {
store, path := newTestFileStore(t)
require.NoError(t, os.WriteFile(path, []byte(`{"data": null}`), 0o600))
// A null data map must read as empty, not crash.
_, err := store.Load("any")
assert.ErrorIs(t, err, ErrNotFound)
require.NoError(t, store.Save("c1", Token{AccessToken: "tok", ClientID: "c1"}))
}
func TestFileStoreWithFileLockPropagatesErrorAndReleases(t *testing.T) {
store, _ := newTestFileStore(t)
sentinel := fmt.Errorf("sentinel failure")
err := store.withFileLock(func() error { return sentinel })
assert.ErrorIs(t, err, sentinel)
// The lock must have been released despite the error: re-acquiring
// immediately must succeed without hitting the timeout.
assert.NoError(t, store.withFileLock(func() error { return nil }))
}
func TestFileStoreSaveLeavesNoTempFile(t *testing.T) {
store, path := newTestFileStore(t)
require.NoError(t, store.Save("c1", Token{AccessToken: "tok", ClientID: "c1"}))
_, err := os.Stat(path + ".tmp")
assert.True(t, os.IsNotExist(err), "temp file left behind after successful save")
}
func TestFileStoreDeleteAbsentKeyDoesNotRewriteFile(t *testing.T) {
store, path := newTestFileStore(t)
require.NoError(t, store.Save("c1", Token{AccessToken: "tok", ClientID: "c1"}))
before, err := os.ReadFile(path)
require.NoError(t, err)
// Deleting a key that is not present must be a no-op write-wise:
// other clients' data stays byte-identical on disk.
require.NoError(t, store.Delete("absent"))
after, err := os.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, string(before), string(after))
}
func TestFileStoreString(t *testing.T) {
store := NewFileStore[Token]("/path/to/tokens.json", JSONCodec[Token]{})
assert.Equal(t, "file: /path/to/tokens.json", store.String())
}
func TestFileStoreNilCodecPanics(t *testing.T) {
assert.Panics(t, func() {
NewFileStore[string]("path", nil)
})
}