mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
Add a system-wide "Backend Mode" that disables user self-registration and self-service while keeping admin panel and API gateway fully functional. When enabled, only admin can log in; all user-facing routes return 403. Backend: - New setting key `backend_mode_enabled` with atomic cached reads (60s TTL) - BackendModeUserGuard middleware blocks non-admin authenticated routes - BackendModeAuthGuard middleware blocks registration/password-reset auth routes - Login/Login2FA/RefreshToken handlers reject non-admin when enabled - TokenPairWithUser struct for role-aware token refresh - 20 unit tests (middleware + service layer) Frontend: - Router guards redirect unauthenticated users to /login - Admin toggle in Settings page - Login page hides register link and footer in backend mode - 9 unit tests for router guard logic - i18n support (en/zh) 27 files changed, 833 insertions(+), 17 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
5.3 KiB
Go
200 lines
5.3 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type bmRepoStub struct {
|
|
getValueFn func(ctx context.Context, key string) (string, error)
|
|
calls int
|
|
}
|
|
|
|
func (s *bmRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
|
panic("unexpected Get call")
|
|
}
|
|
|
|
func (s *bmRepoStub) GetValue(ctx context.Context, key string) (string, error) {
|
|
s.calls++
|
|
if s.getValueFn == nil {
|
|
panic("unexpected GetValue call")
|
|
}
|
|
return s.getValueFn(ctx, key)
|
|
}
|
|
|
|
func (s *bmRepoStub) Set(ctx context.Context, key, value string) error {
|
|
panic("unexpected Set call")
|
|
}
|
|
|
|
func (s *bmRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
|
panic("unexpected GetMultiple call")
|
|
}
|
|
|
|
func (s *bmRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
|
|
panic("unexpected SetMultiple call")
|
|
}
|
|
|
|
func (s *bmRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
|
|
panic("unexpected GetAll call")
|
|
}
|
|
|
|
func (s *bmRepoStub) Delete(ctx context.Context, key string) error {
|
|
panic("unexpected Delete call")
|
|
}
|
|
|
|
type bmUpdateRepoStub struct {
|
|
updates map[string]string
|
|
getValueFn func(ctx context.Context, key string) (string, error)
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
|
panic("unexpected Get call")
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) GetValue(ctx context.Context, key string) (string, error) {
|
|
if s.getValueFn == nil {
|
|
panic("unexpected GetValue call")
|
|
}
|
|
return s.getValueFn(ctx, key)
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) Set(ctx context.Context, key, value string) error {
|
|
panic("unexpected Set call")
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
|
panic("unexpected GetMultiple call")
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
|
|
s.updates = make(map[string]string, len(settings))
|
|
for k, v := range settings {
|
|
s.updates[k] = v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
|
|
panic("unexpected GetAll call")
|
|
}
|
|
|
|
func (s *bmUpdateRepoStub) Delete(ctx context.Context, key string) error {
|
|
panic("unexpected Delete call")
|
|
}
|
|
|
|
func resetBackendModeTestCache(t *testing.T) {
|
|
t.Helper()
|
|
|
|
backendModeCache.Store((*cachedBackendMode)(nil))
|
|
t.Cleanup(func() {
|
|
backendModeCache.Store((*cachedBackendMode)(nil))
|
|
})
|
|
}
|
|
|
|
func TestIsBackendModeEnabled_ReturnsTrue(t *testing.T) {
|
|
resetBackendModeTestCache(t)
|
|
|
|
repo := &bmRepoStub{
|
|
getValueFn: func(ctx context.Context, key string) (string, error) {
|
|
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
|
return "true", nil
|
|
},
|
|
}
|
|
svc := NewSettingService(repo, &config.Config{})
|
|
|
|
require.True(t, svc.IsBackendModeEnabled(context.Background()))
|
|
require.Equal(t, 1, repo.calls)
|
|
}
|
|
|
|
func TestIsBackendModeEnabled_ReturnsFalse(t *testing.T) {
|
|
resetBackendModeTestCache(t)
|
|
|
|
repo := &bmRepoStub{
|
|
getValueFn: func(ctx context.Context, key string) (string, error) {
|
|
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
|
return "false", nil
|
|
},
|
|
}
|
|
svc := NewSettingService(repo, &config.Config{})
|
|
|
|
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
|
require.Equal(t, 1, repo.calls)
|
|
}
|
|
|
|
func TestIsBackendModeEnabled_ReturnsFalseOnNotFound(t *testing.T) {
|
|
resetBackendModeTestCache(t)
|
|
|
|
repo := &bmRepoStub{
|
|
getValueFn: func(ctx context.Context, key string) (string, error) {
|
|
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
|
return "", ErrSettingNotFound
|
|
},
|
|
}
|
|
svc := NewSettingService(repo, &config.Config{})
|
|
|
|
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
|
require.Equal(t, 1, repo.calls)
|
|
}
|
|
|
|
func TestIsBackendModeEnabled_ReturnsFalseOnDBError(t *testing.T) {
|
|
resetBackendModeTestCache(t)
|
|
|
|
repo := &bmRepoStub{
|
|
getValueFn: func(ctx context.Context, key string) (string, error) {
|
|
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
|
return "", errors.New("db down")
|
|
},
|
|
}
|
|
svc := NewSettingService(repo, &config.Config{})
|
|
|
|
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
|
require.Equal(t, 1, repo.calls)
|
|
}
|
|
|
|
func TestIsBackendModeEnabled_CachesResult(t *testing.T) {
|
|
resetBackendModeTestCache(t)
|
|
|
|
repo := &bmRepoStub{
|
|
getValueFn: func(ctx context.Context, key string) (string, error) {
|
|
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
|
return "true", nil
|
|
},
|
|
}
|
|
svc := NewSettingService(repo, &config.Config{})
|
|
|
|
require.True(t, svc.IsBackendModeEnabled(context.Background()))
|
|
require.True(t, svc.IsBackendModeEnabled(context.Background()))
|
|
require.Equal(t, 1, repo.calls)
|
|
}
|
|
|
|
func TestUpdateSettings_InvalidatesBackendModeCache(t *testing.T) {
|
|
resetBackendModeTestCache(t)
|
|
|
|
backendModeCache.Store(&cachedBackendMode{
|
|
value: true,
|
|
expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(),
|
|
})
|
|
|
|
repo := &bmUpdateRepoStub{
|
|
getValueFn: func(ctx context.Context, key string) (string, error) {
|
|
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
|
return "true", nil
|
|
},
|
|
}
|
|
svc := NewSettingService(repo, &config.Config{})
|
|
|
|
err := svc.UpdateSettings(context.Background(), &SystemSettings{
|
|
BackendModeEnabled: false,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "false", repo.updates[SettingKeyBackendModeEnabled])
|
|
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
|
}
|