329 lines
11 KiB
Go
329 lines
11 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"affiliate_dash/internal/model"
|
|
)
|
|
|
|
func TestEnsureTokenValid_RefreshesExpiredToken(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
codec, err := NewSecretCodec("test-master-key")
|
|
if err != nil {
|
|
t.Fatalf("创建 codec 失败: %v", err)
|
|
}
|
|
|
|
var loginCalls int32
|
|
var badBody int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/auth/login":
|
|
atomic.AddInt32(&loginCalls, 1)
|
|
var body map[string]interface{}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
if body["publicShape"] != true || body["username"] != "tester" {
|
|
atomic.AddInt32(&badBody, 1)
|
|
}
|
|
_, _ = w.Write([]byte(`{"code":0,"token":"fresh-token","user":{"username":"tester","role":"user"}}`))
|
|
case "/api/me":
|
|
if r.Header.Get("Authorization") == "Bearer fresh-token" {
|
|
_, _ = w.Write([]byte(`{"user":{"username":"tester","points":"123.5"}}`))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL), codec)
|
|
ctx := context.Background()
|
|
cfg, err := svc.GetConfig(ctx)
|
|
if err != nil {
|
|
t.Fatalf("初始化配置失败: %v", err)
|
|
}
|
|
ciphertext, err := codec.Encrypt("secret-pass")
|
|
if err != nil {
|
|
t.Fatalf("加密密码失败: %v", err)
|
|
}
|
|
if err := db.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(map[string]interface{}{
|
|
"bearer_token": "expired-token",
|
|
"login_username": "tester",
|
|
"login_password_ciphertext": ciphertext,
|
|
}).Error; err != nil {
|
|
t.Fatalf("写入测试配置失败: %v", err)
|
|
}
|
|
|
|
if err := svc.EnsureTokenValid(ctx); err != nil {
|
|
t.Fatalf("Token 自动刷新失败: %v", err)
|
|
}
|
|
if atomic.LoadInt32(&badBody) != 0 {
|
|
t.Fatal("登录请求体缺少 publicShape 或 username")
|
|
}
|
|
if calls := atomic.LoadInt32(&loginCalls); calls != 1 {
|
|
t.Fatalf("应只登录一次,实际 %d", calls)
|
|
}
|
|
|
|
updated, err := svc.GetConfig(ctx)
|
|
if err != nil {
|
|
t.Fatalf("读取配置失败: %v", err)
|
|
}
|
|
if updated.BearerToken != "fresh-token" {
|
|
t.Fatalf("token 未刷新: %q", updated.BearerToken)
|
|
}
|
|
if updated.LastTokenRefreshAt == nil {
|
|
t.Fatal("last_token_refresh_at 未记录")
|
|
}
|
|
if updated.LastTokenError != "" {
|
|
t.Fatalf("刷新成功后不应保留错误: %q", updated.LastTokenError)
|
|
}
|
|
if updated.Points != 123.5 {
|
|
t.Fatalf("积分未同步: %v", updated.Points)
|
|
}
|
|
}
|
|
|
|
func TestEnsureTokenValid_LoginFailureRecordsError(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
codec, _ := NewSecretCodec("test-master-key")
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/auth/login":
|
|
_, _ = w.Write([]byte(`{"code":1,"message":"用户名或密码错误"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL), codec)
|
|
ctx := context.Background()
|
|
cfg, _ := svc.GetConfig(ctx)
|
|
ciphertext, _ := codec.Encrypt("wrong-pass")
|
|
_ = db.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(map[string]interface{}{
|
|
"bearer_token": "expired-token",
|
|
"login_username": "tester",
|
|
"login_password_ciphertext": ciphertext,
|
|
}).Error
|
|
|
|
err := svc.EnsureTokenValid(ctx)
|
|
if err == nil || !strings.Contains(err.Error(), "用户名或密码错误") {
|
|
t.Fatalf("应返回登录失败原因,实际 %v", err)
|
|
}
|
|
updated, _ := svc.GetConfig(ctx)
|
|
if !strings.Contains(updated.LastTokenError, "用户名或密码错误") {
|
|
t.Fatalf("last_token_error 未记录: %q", updated.LastTokenError)
|
|
}
|
|
if updated.BearerToken != "expired-token" {
|
|
t.Fatalf("登录失败不应覆盖 token: %q", updated.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestEnsureTokenValid_NoCredentialsSkipsSilently(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
|
if err := svc.EnsureTokenValid(context.Background()); err != nil {
|
|
t.Fatalf("未配置 token 与账号密码时应静默返回,实际 %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateConfig_EncryptsPasswordAndHidesIt(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
codec, _ := NewSecretCodec("test-master-key")
|
|
svc := NewRockService(db, NewRockClient("https://example.com"), codec)
|
|
ctx := context.Background()
|
|
|
|
username := "tester"
|
|
password := "p@ss-word"
|
|
updated, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{
|
|
LoginUsername: &username,
|
|
LoginPassword: &password,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("保存账号密码失败: %v", err)
|
|
}
|
|
if updated.LoginPasswordCiphertext == "" || updated.LoginPasswordCiphertext == password {
|
|
t.Fatalf("密码未加密落库: %q", updated.LoginPasswordCiphertext)
|
|
}
|
|
if !updated.LoginPasswordSet {
|
|
t.Fatal("login_password_set 应为 true")
|
|
}
|
|
plain, err := codec.Decrypt(updated.LoginPasswordCiphertext)
|
|
if err != nil || plain != password {
|
|
t.Fatalf("密码解密不一致: %q err=%v", plain, err)
|
|
}
|
|
raw, _ := json.Marshal(updated)
|
|
if strings.Contains(string(raw), password) || strings.Contains(string(raw), updated.LoginPasswordCiphertext) {
|
|
t.Fatalf("配置响应泄露了密码: %s", string(raw))
|
|
}
|
|
if !strings.Contains(string(raw), `"login_password_set":true`) {
|
|
t.Fatalf("配置响应缺少 login_password_set: %s", string(raw))
|
|
}
|
|
|
|
// 不传密码时应保持已保存的密文不变。
|
|
same, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{})
|
|
if err != nil {
|
|
t.Fatalf("再次保存失败: %v", err)
|
|
}
|
|
if same.LoginPasswordCiphertext != updated.LoginPasswordCiphertext {
|
|
t.Fatal("未传密码时不应改动已保存密文")
|
|
}
|
|
|
|
empty := ""
|
|
cleared, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{LoginPassword: &empty})
|
|
if err != nil {
|
|
t.Fatalf("清空密码失败: %v", err)
|
|
}
|
|
if cleared.LoginPasswordCiphertext != "" || cleared.LoginPasswordSet {
|
|
t.Fatal("空密码应清空已保存密文")
|
|
}
|
|
}
|
|
|
|
func TestTestLogin_ReturnsUserInfoWithoutSavingToken(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
codec, _ := NewSecretCodec("test-master-key")
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/auth/login":
|
|
_, _ = w.Write([]byte(`{"code":0,"token":"test-token","role":"user","username":"tester"}`))
|
|
case "/api/me":
|
|
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"user":{"username":"tester","points":88.5}}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL), codec)
|
|
ctx := context.Background()
|
|
cfg, _ := svc.GetConfig(ctx)
|
|
ciphertext, _ := codec.Encrypt("secret-pass")
|
|
_ = db.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(map[string]interface{}{
|
|
"login_username": "tester",
|
|
"login_password_ciphertext": ciphertext,
|
|
}).Error
|
|
|
|
result, err := svc.TestLogin(ctx, "", "")
|
|
if err != nil {
|
|
t.Fatalf("测试登录失败: %v", err)
|
|
}
|
|
if result.Username != "tester" || result.Role != "user" || result.Points != 88.5 {
|
|
t.Fatalf("返回信息不符: %+v", result)
|
|
}
|
|
after, _ := svc.GetConfig(ctx)
|
|
if after.BearerToken != "" {
|
|
t.Fatalf("测试登录不应保存 token: %q", after.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestEnsureTokenValid_RefreshesWhenBearerEmpty(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
codec, _ := NewSecretCodec("test-master-key")
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/auth/login":
|
|
_, _ = w.Write([]byte(`{"code":0,"token":"first-token","user":{"username":"tester","role":"user"}}`))
|
|
case "/api/me":
|
|
_, _ = w.Write([]byte(`{"user":{"username":"tester","points":10}}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL), codec)
|
|
ctx := context.Background()
|
|
cfg, _ := svc.GetConfig(ctx)
|
|
ciphertext, _ := codec.Encrypt("secret-pass")
|
|
_ = db.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(map[string]interface{}{
|
|
"login_username": "tester",
|
|
"login_password_ciphertext": ciphertext,
|
|
}).Error
|
|
|
|
if err := svc.EnsureTokenValid(ctx); err != nil {
|
|
t.Fatalf("空 token 时应自动登录: %v", err)
|
|
}
|
|
after, _ := svc.GetConfig(ctx)
|
|
if after.BearerToken != "first-token" {
|
|
t.Fatalf("token 未写入: %q", after.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestRefreshTokenNow_SavesNewToken(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
codec, _ := NewSecretCodec("test-master-key")
|
|
var loginCalls int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/auth/login":
|
|
atomic.AddInt32(&loginCalls, 1)
|
|
_, _ = w.Write([]byte(`{"code":0,"token":"manual-fresh-token","user":{"username":"tester","role":"user"}}`))
|
|
case "/api/me":
|
|
if r.Header.Get("Authorization") != "Bearer manual-fresh-token" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"user":{"username":"tester","points":66.5}}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL), codec)
|
|
ctx := context.Background()
|
|
cfg, _ := svc.GetConfig(ctx)
|
|
ciphertext, _ := codec.Encrypt("secret-pass")
|
|
_ = db.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(map[string]interface{}{
|
|
"bearer_token": "still-valid-token",
|
|
"login_username": "tester",
|
|
"login_password_ciphertext": ciphertext,
|
|
"last_token_refresh_at": nil,
|
|
}).Error
|
|
|
|
result, err := svc.RefreshTokenNow(ctx)
|
|
if err != nil {
|
|
t.Fatalf("手动刷新 Token 失败: %v", err)
|
|
}
|
|
if result.Username != "tester" || result.Role != "user" || result.Points != 66.5 {
|
|
t.Fatalf("返回信息不符: %+v", result)
|
|
}
|
|
if result.RefreshedAt == "" {
|
|
t.Fatal("refreshed_at 未返回")
|
|
}
|
|
if calls := atomic.LoadInt32(&loginCalls); calls != 1 {
|
|
t.Fatalf("应强制登录一次,实际 %d", calls)
|
|
}
|
|
after, _ := svc.GetConfig(ctx)
|
|
if after.BearerToken != "manual-fresh-token" {
|
|
t.Fatalf("token 未更新: %q", after.BearerToken)
|
|
}
|
|
if after.LastTokenRefreshAt == nil {
|
|
t.Fatal("last_token_refresh_at 未更新")
|
|
}
|
|
}
|
|
|
|
func TestRefreshTokenNow_RequiresCredentials(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
|
if _, err := svc.RefreshTokenNow(context.Background()); err == nil ||
|
|
!strings.Contains(err.Error(), "登录账号") {
|
|
t.Fatalf("未配置账号密码时应提示,实际 %v", err)
|
|
}
|
|
}
|