Thirty-seven entries in the root, most of them .go files. The assets had to come along: //go:embed cannot reach outside its own directory, so templates/, static/ and migrations/ live beside the code that embeds them, and testdata/ beside the test that reads it. storage/ stays put — runtime data, not source. go build now needs -o. Without it the output would be named after the package directory and collide with src/ itself.
44 lines
1.1 KiB
Go
44 lines
1.1 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoginRateLimit(t *testing.T) {
|
|
var l limiter
|
|
|
|
for i := range loginMaxFailures - 1 {
|
|
l.fail("[email protected]")
|
|
if l.locked("[email protected]") {
|
|
t.Fatalf("locked after %d failures, limit is %d", i+1, loginMaxFailures)
|
|
}
|
|
}
|
|
l.fail("[email protected]")
|
|
if !l.locked("[email protected]") {
|
|
t.Fatalf("not locked after %d failures", loginMaxFailures)
|
|
}
|
|
|
|
// The limit is per email: locking one address must not lock anyone else out.
|
|
if l.locked("[email protected]") {
|
|
t.Fatal("a different address was locked too")
|
|
}
|
|
|
|
// A correct password clears it, so a member who mistypes nine times and then gets it right
|
|
// starts from zero.
|
|
l.succeed("[email protected]")
|
|
if l.locked("[email protected]") {
|
|
t.Fatal("still locked after a successful login")
|
|
}
|
|
|
|
// Failures older than the window don't accumulate.
|
|
for range loginMaxFailures - 1 {
|
|
l.fail("[email protected]")
|
|
}
|
|
l.by["[email protected]"].first = time.Now().Add(-loginWindow - time.Minute)
|
|
l.fail("[email protected]")
|
|
if l.locked("[email protected]") {
|
|
t.Fatal("failures outside the window were counted")
|
|
}
|
|
}
|