Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
508f246b5b | ||
|
|
1cafaeb953 |
@@ -387,7 +387,7 @@ point of the branch.
|
|||||||
- 2026-03-05 security headers middleware, session regeneration on
|
- 2026-03-05 security headers middleware, session regeneration on
|
||||||
login, request body size limits (#41)
|
login, request body size limits (#41)
|
||||||
- 2026-03-04 tests for delivery, middleware, and session packages
|
- 2026-03-04 tests for delivery, middleware, and session packages
|
||||||
(#32); removed globals.Buildarch (#31)
|
(#32); removed the build-architecture global (#31)
|
||||||
- 2026-03-04 1.0 MVP merge: Webhook/Entrypoint/Target rename, core
|
- 2026-03-04 1.0 MVP merge: Webhook/Entrypoint/Target rename, core
|
||||||
delivery engine with bounded worker pool and circuit breaker,
|
delivery engine with bounded worker pool and circuit breaker,
|
||||||
parallel fan-out, per-webhook event databases, management UI (#16)
|
parallel fan-out, per-webhook event databases, management UI (#16)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@@ -79,3 +80,14 @@ func (d *Database) ExportSetBannerOut(w io.Writer) {
|
|||||||
func DummyPasswordHashForTest() string {
|
func DummyPasswordHashForTest() string {
|
||||||
return dummyPasswordHash()
|
return dummyPasswordHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashAtShippedCostForTest makes HashPassword hash at the shipped
|
||||||
|
// memory cost until t ends. t must not run in parallel with other
|
||||||
|
// tests, which would hash at that cost alongside it.
|
||||||
|
func HashAtShippedCostForTest(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
hashAtShippedCostInTest = true
|
||||||
|
|
||||||
|
t.Cleanup(func() { hashAtShippedCostInTest = false })
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
)
|
)
|
||||||
@@ -63,10 +64,30 @@ func DefaultPasswordConfig() *PasswordConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashPassword generates an Argon2id hash of the password
|
// testArgon2Memory is the Argon2id memory cost, in KiB, that a test
|
||||||
|
// binary hashes with: 1 MB instead of the shipped 64 MB. Every test
|
||||||
|
// that starts a database hashes the bootstrap admin password, dozens
|
||||||
|
// of them run in parallel, and under the race detector each 64 MB hash
|
||||||
|
// holds about 150 MB. VerifyPassword reads the cost from the hash it
|
||||||
|
// checks, so verification follows.
|
||||||
|
const testArgon2Memory = 1024
|
||||||
|
|
||||||
|
// hashAtShippedCostInTest makes a test binary hash at the shipped
|
||||||
|
// memory cost. Only TestHashPassword_ShippedParameters sets it.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // set by one test, see above
|
||||||
|
var hashAtShippedCostInTest bool
|
||||||
|
|
||||||
|
// HashPassword generates an Argon2id hash of the password. A binary
|
||||||
|
// built by go test hashes at testArgon2Memory; one built by go build
|
||||||
|
// always hashes at the defaults.
|
||||||
func HashPassword(password string) (string, error) {
|
func HashPassword(password string) (string, error) {
|
||||||
config := DefaultPasswordConfig()
|
config := DefaultPasswordConfig()
|
||||||
|
|
||||||
|
if testing.Testing() && !hashAtShippedCostInTest {
|
||||||
|
config.Memory = testArgon2Memory
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a salt
|
// Generate a salt
|
||||||
salt := make([]byte, config.SaltLen)
|
salt := make([]byte, config.SaltLen)
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,39 @@ func TestHashPasswordUniqueness(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHashPassword_ShippedParameters hashes and verifies through
|
||||||
|
// HashPassword at the shipped Argon2id parameters. Every other test
|
||||||
|
// hashes at the lower memory cost a test binary uses, so this is the
|
||||||
|
// one that keeps production hashing covered. One hash and one
|
||||||
|
// verification: each costs 64 MB.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // changes the hashing cost for the whole binary
|
||||||
|
func TestHashPassword_ShippedParameters(t *testing.T) {
|
||||||
|
database.HashAtShippedCostForTest(t)
|
||||||
|
|
||||||
|
password := "correct horse battery staple"
|
||||||
|
|
||||||
|
hash, err := database.HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("hashing with the shipped parameters: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shipped = "$argon2id$v=19$m=65536,t=1,p=4$"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(hash, shipped) {
|
||||||
|
t.Errorf("hash = %q, want prefix %q", hash, shipped)
|
||||||
|
}
|
||||||
|
|
||||||
|
valid, err := database.VerifyPassword(password, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VerifyPassword() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
t.Error("VerifyPassword() returned false for correct password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestVerifyDummyPassword_DoesRealWork covers the anti-enumeration
|
// TestVerifyDummyPassword_DoesRealWork covers the anti-enumeration
|
||||||
// path. Login charges an unknown username a verification against a
|
// path. Login charges an unknown username a verification against a
|
||||||
// dummy hash so that a nonexistent account is not answered in
|
// dummy hash so that a nonexistent account is not answered in
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (n *noopEvictor) EvictWebhook(string) {}
|
|||||||
// and the database, exactly as internal/handlers builds them.
|
// and the database, exactly as internal/handlers builds them.
|
||||||
//
|
//
|
||||||
// One application per test function, not per case: every start that
|
// One application per test function, not per case: every start that
|
||||||
// finds no account seeds one at 64 MB of Argon2id, and this package's
|
// finds no account seeds one with an Argon2id hash, and this package's
|
||||||
// budget is not the place to spend that repeatedly.
|
// budget is not the place to spend that repeatedly.
|
||||||
func newServerApp(
|
func newServerApp(
|
||||||
t *testing.T, dir string,
|
t *testing.T, dir string,
|
||||||
|
|||||||
+6
-1
@@ -22,6 +22,11 @@
|
|||||||
# The one figure above 90s is GOMAXPROCS 1, a synthetic core floor rather than
|
# The one figure above 90s is GOMAXPROCS 1, a synthetic core floor rather than
|
||||||
# a condition CI runs under. If a CPU-limited runner ever puts a real run near
|
# a condition CI runs under. If a CPU-limited runner ever puts a real run near
|
||||||
# 67s, that is the datum to revisit the org figure with.
|
# 67s, that is the datum to revisit the org figure with.
|
||||||
|
#
|
||||||
|
# -p 4 -parallel 8 keep the run under 2 GB of memory: at most four test
|
||||||
|
# binaries build or run at once, each with at most eight parallel tests. Under
|
||||||
|
# -race every test binary and every link costs a few hundred MB, so the
|
||||||
|
# defaults (one per core) add up to several GB on a many-core host.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
@@ -29,7 +34,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
"$ROOT/script/assets"
|
"$ROOT/script/assets"
|
||||||
go test -v -race -timeout 90s ./...
|
go test -v -race -p 4 -parallel 8 -timeout 90s ./...
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user