175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
package captcha_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.toowon.com/jimmy/go-common/captcha"
|
|
"git.toowon.com/jimmy/go-common/config"
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
func TestDisabledManager(t *testing.T) {
|
|
m := captcha.NewManager(nil, nil)
|
|
if m.Enabled() {
|
|
t.Fatal("expected disabled")
|
|
}
|
|
_, err := m.Generate(context.Background(), captcha.SceneRegister)
|
|
if err != captcha.ErrDisabled {
|
|
t.Fatalf("Generate err = %v, want ErrDisabled", err)
|
|
}
|
|
}
|
|
|
|
func TestGenerateAndVerify(t *testing.T) {
|
|
s, err := miniredis.Run()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
rdb := redis.NewClient(&redis.Options{Addr: s.Addr()})
|
|
cfg := &config.CaptchaConfig{
|
|
Enabled: true,
|
|
Length: 4,
|
|
Width: 120,
|
|
Height: 40,
|
|
TTLSec: 300,
|
|
Login: &config.LoginCaptchaConfig{},
|
|
}
|
|
m := captcha.NewManager(rdb, cfg)
|
|
if !m.Enabled() {
|
|
t.Fatal("expected enabled")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
result, err := m.Generate(ctx, captcha.SceneRegister)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.ID == "" || result.ImageBase64 == "" {
|
|
t.Fatal("expected id and image")
|
|
}
|
|
|
|
// 从 redis 读取答案用于测试
|
|
answer, err := rdb.Get(ctx, "captcha:code:"+result.ID).Result()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Verify(ctx, captcha.VerifyRequest{
|
|
Scene: captcha.SceneRegister,
|
|
ID: result.ID,
|
|
Answer: answer,
|
|
}); err != nil {
|
|
t.Fatalf("Verify: %v", err)
|
|
}
|
|
if err := m.Verify(ctx, captcha.VerifyRequest{
|
|
Scene: captcha.SceneRegister,
|
|
ID: result.ID,
|
|
Answer: answer,
|
|
}); err != captcha.ErrInvalid {
|
|
t.Fatalf("second Verify err = %v, want ErrInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestLoginGuard(t *testing.T) {
|
|
s, err := miniredis.Run()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
rdb := redis.NewClient(&redis.Options{Addr: s.Addr()})
|
|
cfg := &config.CaptchaConfig{
|
|
Enabled: true,
|
|
Length: 4,
|
|
Width: 120,
|
|
Height: 40,
|
|
TTLSec: 300,
|
|
Login: &config.LoginCaptchaConfig{
|
|
ShowAfterFailures: 2,
|
|
LockAfterFailures: 4,
|
|
LockDurationMin: 1,
|
|
},
|
|
}
|
|
m := captcha.NewManager(rdb, cfg)
|
|
ctx := context.Background()
|
|
key := "alice"
|
|
|
|
status, err := m.RecordLoginFailure(ctx, key)
|
|
if err != nil || status.FailCount != 1 || status.NeedCaptcha {
|
|
t.Fatalf("first failure: status=%+v err=%v", status, err)
|
|
}
|
|
|
|
status, err = m.RecordLoginFailure(ctx, key)
|
|
if err != nil || status.FailCount != 2 || !status.NeedCaptcha {
|
|
t.Fatalf("second failure: status=%+v err=%v", status, err)
|
|
}
|
|
|
|
need, err := m.LoginNeedCaptcha(ctx, key)
|
|
if err != nil || !need {
|
|
t.Fatalf("LoginNeedCaptcha = %v, err = %v", need, err)
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
_, _ = m.RecordLoginFailure(ctx, key)
|
|
}
|
|
status, err = m.RecordLoginFailure(ctx, key)
|
|
if err != captcha.ErrLoginLocked || !status.Locked {
|
|
t.Fatalf("lock: status=%+v err=%v", status, err)
|
|
}
|
|
|
|
locked, remaining, err := m.IsLoginLocked(ctx, key)
|
|
if err != nil || !locked || remaining <= 0 {
|
|
t.Fatalf("IsLoginLocked = %v, %d, err = %v", locked, remaining, err)
|
|
}
|
|
|
|
if err := m.ClearLoginFailures(ctx, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
locked, _, _ = m.IsLoginLocked(ctx, key)
|
|
if locked {
|
|
t.Fatal("expected unlocked after clear")
|
|
}
|
|
}
|
|
|
|
func TestSceneRequiresCaptcha(t *testing.T) {
|
|
cfg := &config.CaptchaConfig{
|
|
Enabled: true,
|
|
Scenes: map[string]config.CaptchaSceneConfig{
|
|
"register": {Mode: "always"},
|
|
"login": {Mode: "never"},
|
|
},
|
|
}
|
|
m := captcha.NewManager(redis.NewClient(&redis.Options{Addr: "127.0.0.1:1"}), cfg)
|
|
if !m.SceneRequiresCaptcha(captcha.SceneRegister) {
|
|
t.Fatal("register should require captcha")
|
|
}
|
|
if m.SceneRequiresCaptcha(captcha.SceneLogin) {
|
|
t.Fatal("login should not always require captcha")
|
|
}
|
|
}
|
|
|
|
func TestRedisStoreTTL(t *testing.T) {
|
|
s, err := miniredis.Run()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
rdb := redis.NewClient(&redis.Options{Addr: s.Addr()})
|
|
cfg := &config.CaptchaConfig{Enabled: true, TTLSec: 1, Login: &config.LoginCaptchaConfig{}}
|
|
m := captcha.NewManager(rdb, cfg)
|
|
ctx := context.Background()
|
|
|
|
result, err := m.Generate(ctx, captcha.SceneRegister)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.FastForward(2 * time.Second)
|
|
if err := m.Verify(ctx, captcha.VerifyRequest{ID: result.ID, Answer: "x"}); err != captcha.ErrInvalid {
|
|
t.Fatalf("expired verify err = %v", err)
|
|
}
|
|
}
|