You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
7.9 KiB
278 lines
7.9 KiB
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseSliderSteps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
size, swaps, attempts, err := parseSliderSteps([]int{5, 0, 1, 2, 3, 7})
|
|
if err != nil {
|
|
t.Fatalf("parseSliderSteps returned error: %v", err)
|
|
}
|
|
|
|
if size != 5 {
|
|
t.Fatalf("expected size 5, got %d", size)
|
|
}
|
|
if attempts != 7 {
|
|
t.Fatalf("expected attempts 7, got %d", attempts)
|
|
}
|
|
expected := []int{0, 1, 2, 3}
|
|
if !reflect.DeepEqual(swaps, expected) {
|
|
t.Fatalf("expected swaps %v, got %v", expected, swaps)
|
|
}
|
|
}
|
|
|
|
func TestParseCaptchaSettingsResponseSupportsJSONStringMap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
resp := map[string]interface{}{
|
|
"response": map[string]interface{}{
|
|
"show_captcha_type": "checkbox",
|
|
"captcha_settings": `{"slider":"slider-token","sound":"sound-token"}`,
|
|
},
|
|
}
|
|
|
|
settings, err := parseCaptchaSettingsResponse(resp)
|
|
if err != nil {
|
|
t.Fatalf("parseCaptchaSettingsResponse returned error: %v", err)
|
|
}
|
|
|
|
if settings.ShowCaptchaType != "checkbox" {
|
|
t.Fatalf("expected show_captcha_type checkbox, got %q", settings.ShowCaptchaType)
|
|
}
|
|
if settings.SettingsByType["slider"] != "slider-token" {
|
|
t.Fatalf("expected slider settings token, got %q", settings.SettingsByType["slider"])
|
|
}
|
|
if settings.SettingsByType["sound"] != "sound-token" {
|
|
t.Fatalf("expected sound settings token, got %q", settings.SettingsByType["sound"])
|
|
}
|
|
}
|
|
|
|
func TestParseCaptchaBootstrapHTML(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
html := `
|
|
<script>
|
|
window.init = {"data":{"show_captcha_type":"checkbox","captcha_settings":[{"type":"slider","settings":"slider-token"},{"type":"sound","settings":"sound-token"}]}};
|
|
window.lang = {};
|
|
</script>
|
|
<script>
|
|
const powInput = "abc123";
|
|
const difficulty = 3;
|
|
</script>`
|
|
|
|
bootstrap, err := parseCaptchaBootstrapHTML(html)
|
|
if err != nil {
|
|
t.Fatalf("parseCaptchaBootstrapHTML returned error: %v", err)
|
|
}
|
|
|
|
if bootstrap.PowInput != "abc123" {
|
|
t.Fatalf("expected pow input abc123, got %q", bootstrap.PowInput)
|
|
}
|
|
if bootstrap.Difficulty != 3 {
|
|
t.Fatalf("expected difficulty 3, got %d", bootstrap.Difficulty)
|
|
}
|
|
if bootstrap.Settings == nil {
|
|
t.Fatal("expected bootstrap settings")
|
|
}
|
|
if bootstrap.Settings.ShowCaptchaType != "checkbox" {
|
|
t.Fatalf("expected show_captcha_type checkbox, got %q", bootstrap.Settings.ShowCaptchaType)
|
|
}
|
|
if bootstrap.Settings.SettingsByType["slider"] != "slider-token" {
|
|
t.Fatalf("expected slider token, got %q", bootstrap.Settings.SettingsByType["slider"])
|
|
}
|
|
}
|
|
|
|
func TestRenderSliderCandidateMatchesSwapLayout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
src := image.NewRGBA(image.Rect(0, 0, 20, 20))
|
|
fillRect(src, image.Rect(0, 0, 10, 10), color.RGBA{R: 255, A: 255})
|
|
fillRect(src, image.Rect(10, 0, 20, 10), color.RGBA{G: 255, A: 255})
|
|
fillRect(src, image.Rect(0, 10, 10, 20), color.RGBA{B: 255, A: 255})
|
|
fillRect(src, image.Rect(10, 10, 20, 20), color.RGBA{R: 255, G: 255, A: 255})
|
|
|
|
mapping, err := buildSliderTileMapping(2, []int{0, 1})
|
|
if err != nil {
|
|
t.Fatalf("buildSliderTileMapping returned error: %v", err)
|
|
}
|
|
|
|
rendered, err := renderSliderCandidate(src, 2, mapping)
|
|
if err != nil {
|
|
t.Fatalf("renderSliderCandidate returned error: %v", err)
|
|
}
|
|
|
|
assertPixelEquals(t, rendered.At(2, 2), color.RGBA{G: 255, A: 255})
|
|
assertPixelEquals(t, rendered.At(12, 2), color.RGBA{R: 255, A: 255})
|
|
assertPixelEquals(t, rendered.At(2, 12), color.RGBA{B: 255, A: 255})
|
|
assertPixelEquals(t, rendered.At(12, 12), color.RGBA{R: 255, G: 255, A: 255})
|
|
}
|
|
|
|
func TestRankSliderCandidatesPrefersMostCoherentImage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
src := image.NewRGBA(image.Rect(0, 0, 30, 30))
|
|
for y := 0; y < 30; y++ {
|
|
for x := 0; x < 30; x++ {
|
|
src.Set(x, y, color.RGBA{
|
|
R: uint8(x * 5),
|
|
G: uint8(y * 5),
|
|
B: uint8((x + y) * 3),
|
|
A: 255,
|
|
})
|
|
}
|
|
}
|
|
|
|
candidates, err := rankSliderCandidates(src, 3, []int{0, 1, 0, 1})
|
|
if err != nil {
|
|
t.Fatalf("rankSliderCandidates returned error: %v", err)
|
|
}
|
|
|
|
if len(candidates) != 2 {
|
|
t.Fatalf("expected 2 candidates, got %d", len(candidates))
|
|
}
|
|
if candidates[0].Index != 2 {
|
|
t.Fatalf("expected solved candidate to rank first, got candidate %d", candidates[0].Index)
|
|
}
|
|
}
|
|
|
|
func TestEncodeSliderAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
encoded, err := encodeSliderAnswer([]int{9, 10, 2})
|
|
if err != nil {
|
|
t.Fatalf("encodeSliderAnswer returned error: %v", err)
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
t.Fatalf("failed to decode answer: %v", err)
|
|
}
|
|
|
|
var payload struct {
|
|
Value []int `json:"value"`
|
|
}
|
|
if err := json.Unmarshal(decoded, &payload); err != nil {
|
|
t.Fatalf("failed to unmarshal answer payload: %v", err)
|
|
}
|
|
|
|
expected := []int{9, 10, 2}
|
|
if !reflect.DeepEqual(payload.Value, expected) {
|
|
t.Fatalf("expected payload %v, got %v", expected, payload.Value)
|
|
}
|
|
}
|
|
|
|
func TestTrySliderCaptchaCandidates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
candidates := []sliderCandidate{
|
|
{Index: 1, ActiveSteps: []int{0, 1}, Score: 10},
|
|
{Index: 2, ActiveSteps: []int{0, 1, 0, 1}, Score: 20},
|
|
}
|
|
|
|
t.Run("success on first candidate", func(t *testing.T) {
|
|
token, err := trySliderCaptchaCandidates(candidates, 2, func(candidate sliderCandidate) (*captchaCheckResult, error) {
|
|
if candidate.Index != 1 {
|
|
t.Fatalf("unexpected candidate index %d", candidate.Index)
|
|
}
|
|
return &captchaCheckResult{Status: "OK", SuccessToken: "token-1"}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("trySliderCaptchaCandidates returned error: %v", err)
|
|
}
|
|
if token != "token-1" {
|
|
t.Fatalf("expected token-1, got %s", token)
|
|
}
|
|
})
|
|
|
|
t.Run("success on later candidate", func(t *testing.T) {
|
|
calls := 0
|
|
token, err := trySliderCaptchaCandidates(candidates, 2, func(candidate sliderCandidate) (*captchaCheckResult, error) {
|
|
calls++
|
|
if candidate.Index == 1 {
|
|
return &captchaCheckResult{Status: "BOT"}, nil
|
|
}
|
|
return &captchaCheckResult{Status: "OK", SuccessToken: "token-2"}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("trySliderCaptchaCandidates returned error: %v", err)
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("expected 2 calls, got %d", calls)
|
|
}
|
|
if token != "token-2" {
|
|
t.Fatalf("expected token-2, got %s", token)
|
|
}
|
|
})
|
|
|
|
t.Run("exhausted candidates", func(t *testing.T) {
|
|
_, err := trySliderCaptchaCandidates(candidates, 1, func(candidate sliderCandidate) (*captchaCheckResult, error) {
|
|
return &captchaCheckResult{Status: "BOT"}, nil
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error after exhausting ranked candidates")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseSliderCaptchaContentResponse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
src := image.NewRGBA(image.Rect(0, 0, 20, 20))
|
|
fillRect(src, src.Bounds(), color.RGBA{R: 12, G: 34, B: 56, A: 255})
|
|
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, src, nil); err != nil {
|
|
t.Fatalf("failed to encode jpeg fixture: %v", err)
|
|
}
|
|
|
|
resp := map[string]interface{}{
|
|
"response": map[string]interface{}{
|
|
"status": "OK",
|
|
"extension": "jpeg",
|
|
"image": base64.StdEncoding.EncodeToString(buf.Bytes()),
|
|
"steps": []interface{}{float64(5), float64(0), float64(1), float64(2), float64(3), float64(6)},
|
|
},
|
|
}
|
|
|
|
content, err := parseSliderCaptchaContentResponse(resp)
|
|
if err != nil {
|
|
t.Fatalf("parseSliderCaptchaContentResponse returned error: %v", err)
|
|
}
|
|
|
|
if content.Size != 5 {
|
|
t.Fatalf("expected size 5, got %d", content.Size)
|
|
}
|
|
if content.Attempts != 6 {
|
|
t.Fatalf("expected attempts 6, got %d", content.Attempts)
|
|
}
|
|
if len(content.Steps) != 4 {
|
|
t.Fatalf("expected 4 swap entries, got %d", len(content.Steps))
|
|
}
|
|
}
|
|
|
|
func fillRect(img *image.RGBA, rect image.Rectangle, c color.Color) {
|
|
for y := rect.Min.Y; y < rect.Max.Y; y++ {
|
|
for x := rect.Min.X; x < rect.Max.X; x++ {
|
|
img.Set(x, y, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertPixelEquals(t *testing.T, actual color.Color, expected color.RGBA) {
|
|
t.Helper()
|
|
|
|
ar, ag, ab, aa := actual.RGBA()
|
|
if ar != uint32(expected.R)*0x101 || ag != uint32(expected.G)*0x101 || ab != uint32(expected.B)*0x101 || aa != uint32(expected.A)*0x101 {
|
|
t.Fatalf("expected pixel %+v, got rgba(%d,%d,%d,%d)", expected, ar, ag, ab, aa)
|
|
}
|
|
}
|
|
|