Files
gitea-tea/modules/config/lock_test.go
2026-02-03 23:48:18 +00:00

183 lines
4.4 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package config
import (
"fmt"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
func TestConfigLock_BasicLockUnlock(t *testing.T) {
// Create a temp directory for test
tmpDir, err := os.MkdirTemp("", "tea-lock-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
lockPath := filepath.Join(tmpDir, "config.yml.lock")
// Should be able to acquire lock
unlock, err := acquireConfigLock(lockPath, 5*time.Second)
if err != nil {
t.Fatalf("failed to acquire lock: %v", err)
}
// Should be able to release lock
err = unlock()
if err != nil {
t.Fatalf("failed to release lock: %v", err)
}
}
func TestConfigLock_MutexProtection(t *testing.T) {
// Create a temp directory for test
tmpDir, err := os.MkdirTemp("", "tea-lock-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
lockPath := filepath.Join(tmpDir, "config.yml.lock")
// Acquire lock
unlock, err := acquireConfigLock(lockPath, 5*time.Second)
if err != nil {
t.Fatalf("failed to acquire lock: %v", err)
}
// Try to acquire again from same process - should block/timeout due to mutex
done := make(chan bool)
go func() {
_, err := acquireConfigLock(lockPath, 100*time.Millisecond)
done <- (err != nil) // Should timeout/fail
}()
select {
case failed := <-done:
if !failed {
t.Error("second lock acquisition should have failed due to mutex")
}
case <-time.After(2 * time.Second):
t.Error("test timed out")
}
if err := unlock(); err != nil {
t.Errorf("failed to unlock: %v", err)
}
}
func TestReloadConfigFromDisk(t *testing.T) {
// Save original config state
originalConfig := config
// Create a temp config file
tmpDir, err := os.MkdirTemp("", "tea-reload-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// We can't easily change GetConfigPath, so we test that reloadConfigFromDisk
// handles a missing file gracefully (returns nil and resets config)
config = LocalConfig{Logins: []Login{{Name: "test"}}}
// Call reload - since the actual config path likely exists or doesn't,
// we just verify it doesn't panic and returns without error or with expected error
err = reloadConfigFromDisk()
// The function should either succeed or return an error, not panic
if err != nil {
// This is acceptable - config file might not exist in test environment
t.Logf("reloadConfigFromDisk returned error (expected in test env): %v", err)
}
// Restore original config
config = originalConfig
}
func TestWithConfigLock(t *testing.T) {
executed := false
err := withConfigLock(func() error {
executed = true
return nil
})
if err != nil {
t.Errorf("withConfigLock returned error: %v", err)
}
if !executed {
t.Error("function was not executed")
}
}
func TestWithConfigLock_PropagatesError(t *testing.T) {
expectedErr := fmt.Errorf("test error")
err := withConfigLock(func() error {
return expectedErr
})
if err != expectedErr {
t.Errorf("expected error %v, got %v", expectedErr, err)
}
}
func TestDoubleCheckedLocking_SimulatedRefresh(t *testing.T) {
// This test simulates the double-checked locking pattern
// by having multiple goroutines try to "refresh" simultaneously
var (
refreshCount int
mu sync.Mutex
)
// Simulate what RefreshOAuthToken does with double-check
simulatedRefresh := func(tokenExpiry *int64) error {
// First check (without lock)
if *tokenExpiry > time.Now().Unix() {
return nil // Token still valid
}
return withConfigLock(func() error {
// Double-check after acquiring lock
if *tokenExpiry > time.Now().Unix() {
return nil // Another goroutine refreshed it
}
// Simulate refresh
mu.Lock()
refreshCount++
mu.Unlock()
time.Sleep(50 * time.Millisecond) // Simulate API call
*tokenExpiry = time.Now().Add(1 * time.Hour).Unix()
return nil
})
}
// Start with expired token
tokenExpiry := time.Now().Add(-1 * time.Hour).Unix()
// Launch multiple goroutines trying to refresh
var wg sync.WaitGroup
for range 5 {
wg.Add(1)
go func() {
defer wg.Done()
if err := simulatedRefresh(&tokenExpiry); err != nil {
t.Errorf("refresh failed: %v", err)
}
}()
}
wg.Wait()
// Should only have refreshed once due to double-checked locking
if refreshCount != 1 {
t.Errorf("expected 1 refresh, got %d", refreshCount)
}
}