Files
affiliate_dash/backend/internal/service/rock_login_test.go
T

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)
}
}