| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- package fwh_pro
- import (
- "crypto/md5"
- "encoding/hex"
- "encoding/json"
- "fwh-toolbox-server/utils/httpClient"
- "strings"
- "testing"
- "time"
- )
- func loginTestResponse(data any) *httpClient.Response {
- body, _ := json.Marshal(map[string]any{"code": 200, "success": true, "data": data})
- return &httpClient.Response{StatusCode: 200, Body: body}
- }
- func withLoginTestConfig(t *testing.T, secret string) {
- t.Helper()
- oldUser, oldPassword, oldSecret, oldInfo := fwhUser, fwhPassword, fwhTotpSecret, loginInfo
- passwordHash := md5.Sum([]byte("plain-password"))
- fwhUser, fwhPassword, fwhTotpSecret = "user", hex.EncodeToString(passwordHash[:]), secret
- t.Cleanup(func() {
- fwhUser, fwhPassword, fwhTotpSecret, loginInfo = oldUser, oldPassword, oldSecret, oldInfo
- })
- }
- func TestLoginDirectWithoutTOTP(t *testing.T) {
- withLoginTestConfig(t, "")
- calls := 0
- post := func(path string, body any, clientID string) (*httpClient.Response, error) {
- calls++
- if path != "/api-user/user/login" || clientID != "device-1" {
- t.Fatalf("unexpected request: %s, %s", path, clientID)
- }
- payload, ok := body.(LoginPayload)
- if !ok || payload.Password != fwhPassword || payload.Timezone != "Asia/Shanghai" || payload.Lang != "ZH_CN" {
- t.Fatalf("unexpected login payload: %+v", payload)
- }
- return loginTestResponse(map[string]any{"userId": 7, "token": "direct-token"}), nil
- }
- if err := loginWithPost(post, time.Now, "device-1"); err != nil {
- t.Fatal(err)
- }
- if calls != 1 || loginInfo.Token != "direct-token" || loginInfo.UserId != 7 {
- t.Fatalf("direct login result: calls=%d, token=%q, userId=%d", calls, loginInfo.Token, loginInfo.UserId)
- }
- }
- func TestLoginTOTPFromMFAResponse(t *testing.T) {
- withLoginTestConfig(t, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
- calls := 0
- post := func(path string, body any, clientID string) (*httpClient.Response, error) {
- calls++
- if clientID != "device-1" {
- t.Fatalf("client ID changed between login steps")
- }
- switch calls {
- case 1:
- if path != "/api-user/user/login" {
- t.Fatalf("unexpected first path: %s", path)
- }
- return loginTestResponse(map[string]any{"mfaRequired": true, "mfaToken": "temporary-token", "mfaMethod": "TOTP"}), nil
- case 2:
- verify, ok := body.(MfaVerifyPayload)
- if path != "/api-user/mfa/login/verify" || !ok || verify.MfaToken != "temporary-token" || verify.Code != "287082" || verify.Method != "TOTP" || verify.DeviceFingerprint != clientID || verify.RememberDevice != "" {
- t.Fatalf("unexpected verify request: path=%s, body=%+v", path, body)
- }
- return loginTestResponse(map[string]any{"userId": 9, "token": "verified-token"}), nil
- default:
- t.Fatalf("too many login requests")
- return nil, nil
- }
- }
- if err := loginWithPost(post, func() time.Time { return time.Unix(59, 0) }, "device-1"); err != nil {
- t.Fatal(err)
- }
- if calls != 2 || loginInfo.Token != "verified-token" || loginInfo.UserId != 9 {
- t.Fatalf("TOTP login result: calls=%d, token=%q, userId=%d", calls, loginInfo.Token, loginInfo.UserId)
- }
- }
- func TestLoginRequiresConfiguredTOTPWhenMFARequested(t *testing.T) {
- withLoginTestConfig(t, "")
- loginInfo.Token = "old-token"
- calls := 0
- post := func(path string, body any, clientID string) (*httpClient.Response, error) {
- calls++
- return loginTestResponse(map[string]any{"mfaRequired": true, "mfaToken": "temporary-token", "mfaMethod": "TOTP"}), nil
- }
- err := loginWithPost(post, time.Now, "device-1")
- if err == nil || !strings.Contains(err.Error(), "FWH_TOTP_SECRET") || calls != 1 || loginInfo.Token != "" {
- t.Fatalf("missing TOTP handling: err=%v, calls=%d, token=%q", err, calls, loginInfo.Token)
- }
- }
- func TestLoginWithConfiguredSecretAcceptsDirectToken(t *testing.T) {
- withLoginTestConfig(t, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
- calls := 0
- post := func(path string, body any, clientID string) (*httpClient.Response, error) {
- calls++
- return loginTestResponse(map[string]any{"userId": 4, "token": "direct-token"}), nil
- }
- if err := loginWithPost(post, time.Now, "device-1"); err != nil || calls != 1 || loginInfo.Token != "direct-token" {
- t.Fatalf("direct response with secret: err=%v, calls=%d, token=%q", err, calls, loginInfo.Token)
- }
- }
- func TestLoginRejectedTOTPDoesNotReuseOldToken(t *testing.T) {
- withLoginTestConfig(t, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
- loginInfo.Token = "old-token"
- calls := 0
- post := func(path string, body any, clientID string) (*httpClient.Response, error) {
- calls++
- if calls == 1 {
- return loginTestResponse(map[string]any{"mfaRequired": true, "mfaToken": "temporary-token", "mfaMethod": "TOTP"}), nil
- }
- return &httpClient.Response{StatusCode: 200, Body: []byte(`{"code":401,"success":false}`)}, nil
- }
- err := loginWithPost(post, func() time.Time { return time.Unix(59, 0) }, "device-1")
- if err == nil || calls != 2 || loginInfo.Token != "" {
- t.Fatalf("rejected TOTP handling: err=%v, calls=%d, token=%q", err, calls, loginInfo.Token)
- }
- }
|