Parallelize unit tests

This commit is contained in:
Jonathon Leight
2026-06-22 11:31:13 -04:00
parent f40ca97393
commit eeece7caaa
31 changed files with 551 additions and 167 deletions
+1 -1
View File
@@ -19,6 +19,6 @@ steps:
commands:
- until bash -c 'echo > /dev/tcp/postgres/5432' 2>/dev/null; do sleep 1; done
- go vet ./...
- go test -race -p 1 ./...
- go test -race ./...
depends_on: []
+13 -7
View File
@@ -88,17 +88,23 @@ must serve the confirm/control pages over HTTPS.
## Tests
```sh
go test ./... # unit tests (crypto round-trip, seal/open) — no DB needed
go test ./...
```
The end-to-end confirm round-trip (browser + modem + repeater simulated in-process) is gated on a
**dedicated** test database, since it truncates all tables. The database name **must end in
`_test`** — the test refuses to run otherwise, so it can never wipe your dev data:
That's it — **just make sure Docker is running.** DB-backed tests (store queries and the
end-to-end confirm/console round-trips) spin up a throwaway `postgres:17` container automatically
via [testcontainers](https://golang.testcontainers.org/). The harness migrates a single template
database once, then clones a fresh database per test (`CREATE DATABASE … TEMPLATE …`), so every
test gets pristine, isolated state and tears it down when it finishes. No env vars, no manual
database setup, nothing to wipe.
To run against an **existing** Postgres instead of a container (this is how CI reuses its service
container), set `MESHTENDER_TEST_DATABASE_URL` to a DSN on that server. The connecting role needs
`CREATEDB`, and the harness only ever creates/drops its own `mt_tmpl_*` / `mt_test_*` databases:
```sh
docker exec <pg-container> psql -U meshtender -c 'CREATE DATABASE meshtender_test OWNER meshtender;'
MESHTENDER_TEST_DATABASE_URL="postgres://meshtender:meshtender@localhost:5432/meshtender_test?sslmode=disable" \
go test ./internal/web/ -run TestConfirmRoundTrip -v
MESHTENDER_TEST_DATABASE_URL="postgres://meshtender:meshtender@localhost:5432/postgres?sslmode=disable" \
go test ./internal/core/ -run TestConfirmRoundTrip -v
```
## Layout
+49
View File
@@ -11,12 +11,32 @@ require (
github.com/jackc/pgx/v5 v5.10.0
github.com/meshcore-go/meshcore-go v1.0.8
github.com/pressly/goose/v3 v3.27.1
github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0
golang.org/x/crypto v0.53.0
)
require (
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
@@ -25,13 +45,42 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.5 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/testcontainers/testcontainers-go v0.43.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+124
View File
@@ -1,21 +1,62 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alexedwards/scs/pgxstore v0.0.0-20251002162104-209de6e426de h1:wNJVpr0ag/BL2nRGBIESdLe1qoljXIolF/qPi1gleRA=
github.com/alexedwards/scs/pgxstore v0.0.0-20251002162104-209de6e426de/go.mod h1:hwveArYcjyOK66EViVgVU5Iqj7zyEsWjKXMQhDJrTLI=
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
@@ -24,6 +65,9 @@ github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
@@ -41,34 +85,79 @@ github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QII
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
github.com/meshcore-go/meshcore-go v1.0.8 h1:NPmvE6U6upFBVjyoA9jTj6X8MknSnslv767FeZ/KSXI=
github.com/meshcore-go/meshcore-go v1.0.8/go.mod h1:EnLNJNCjGLXoX+RHQXhAqu6LJkIVTkw9Jxpxre6HLJc=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -76,11 +165,35 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0 h1:ShNOFYAF4lKHvdIG258hi69bSxC88uXnxJkJvNs/IVs=
github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0/go.mod h1:vdq5/RqmGfWeefzyfcVI/pID1rzmc1TDvqXa15bPJks=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -103,8 +216,11 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -117,6 +233,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -130,13 +248,17 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@@ -145,3 +267,5 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
+1
View File
@@ -3,6 +3,7 @@ package auth
import "testing"
func TestSafeLocalPath(t *testing.T) {
t.Parallel()
cases := []struct {
path string
want bool
+2 -25
View File
@@ -9,7 +9,6 @@ import (
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -29,30 +28,8 @@ import (
// (MeshCore crypto). It is gated on MESHTENDER_TEST_DATABASE_URL so a plain
// `go test` never truncates a real database.
func TestConfirmRoundTrip(t *testing.T) {
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run this integration test")
}
// Guardrail: this test TRUNCATEs every table, so it must never point at a
// real/dev database. Require a database name ending in "_test".
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: MESHTENDER_TEST_DATABASE_URL must use a database named *_test (got %q) — it truncates all tables", dsn)
}
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
t.Fatalf("store: %v", err)
}
defer st.Close()
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
// Isolate: wipe all state.
if _, err := st.Pool().Exec(ctx,
`TRUNCATE users, repeaters, repeater_shares, webauthn_credentials, server_identity, sessions RESTART IDENTITY CASCADE`); err != nil {
t.Fatalf("truncate: %v", err)
}
t.Parallel()
st, ctx := coreStore(t)
var masterKey [32]byte
_, _ = rand.Read(masterKey[:])
+3 -21
View File
@@ -8,8 +8,6 @@ import (
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -25,6 +23,7 @@ import (
)
func TestParseLocationFloat(t *testing.T) {
t.Parallel()
cases := map[string]struct {
want float64
ok bool
@@ -46,25 +45,8 @@ func TestParseLocationFloat(t *testing.T) {
// TestConfirmFetchesLocation drives the confirm flow with store_location set and
// verifies the repeater's lat/lon are fetched (get lat / get lon) and stored.
func TestConfirmFetchesLocation(t *testing.T) {
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run this integration test")
}
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: test DB name must end in _test (got %q)", dsn)
}
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
t.Fatalf("store: %v", err)
}
defer st.Close()
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
if _, err := st.Pool().Exec(ctx, `TRUNCATE users, repeaters, organizations, server_identity, sessions RESTART IDENTITY CASCADE`); err != nil {
t.Fatalf("truncate: %v", err)
}
t.Parallel()
st, ctx := coreStore(t)
var masterKey [32]byte
_, _ = rand.Read(masterKey[:])
+2 -27
View File
@@ -8,8 +8,6 @@ import (
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -27,31 +25,8 @@ import (
// TestConfirmLoginRetry drops the first login (simulating a lost packet) and
// verifies the confirm flow retries with a fresh timestamp and succeeds.
func TestConfirmLoginRetry(t *testing.T) {
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run this integration test")
}
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: test DB name must end in _test (got %q)", dsn)
}
// Shorten the per-attempt reply wait for the test.
orig := perTryReply
perTryReply = 300 * time.Millisecond
defer func() { perTryReply = orig }()
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
t.Fatalf("store: %v", err)
}
defer st.Close()
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
if _, err := st.Pool().Exec(ctx, `TRUNCATE users, repeaters, organizations, server_identity, sessions RESTART IDENTITY CASCADE`); err != nil {
t.Fatalf("truncate: %v", err)
}
t.Parallel()
st, ctx := coreStore(t)
var masterKey [32]byte
_, _ = rand.Read(masterKey[:])
+2 -26
View File
@@ -8,8 +8,6 @@ import (
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -28,30 +26,8 @@ import (
// browser (WebSocket), the KISS modem (framing), and the repeater (decrypts the
// command, replies). Gated on MESHTENDER_TEST_DATABASE_URL (db name ends _test).
func TestConsoleRoundTrip(t *testing.T) {
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run this integration test")
}
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: test DB name must end in _test (got %q)", dsn)
}
orig := perTryReply
perTryReply = 300 * time.Millisecond
defer func() { perTryReply = orig }()
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
t.Fatalf("store: %v", err)
}
defer st.Close()
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
if _, err := st.Pool().Exec(ctx,
`TRUNCATE users, repeaters, repeater_shares, repeater_invites, share_commands, command_log, webauthn_credentials, server_identity, sessions RESTART IDENTITY CASCADE`); err != nil {
t.Fatalf("truncate: %v", err)
}
t.Parallel()
st, ctx := coreStore(t)
var masterKey [32]byte
_, _ = rand.Read(masterKey[:])
+5 -19
View File
@@ -1,10 +1,7 @@
package core
import (
"context"
"fmt"
"net/url"
"os"
"regexp"
"strings"
"testing"
@@ -46,6 +43,7 @@ func resolveTestCatalog() []*store.Command {
}
func TestResolveCommand(t *testing.T) {
t.Parallel()
cat := resolveTestCatalog()
cases := []struct {
typed, want string // want == "" means must be denied (nil)
@@ -120,6 +118,7 @@ func TestResolveCommand(t *testing.T) {
}
func TestValidCommandText(t *testing.T) {
t.Parallel()
ok := []string{"ver", "set tx 20", "set name My Repeater", "setperm abc 3"}
for _, s := range ok {
if !validCommandText(s) {
@@ -148,6 +147,7 @@ func TestValidCommandText(t *testing.T) {
// the end — this catches the Go list drifting from the DB). Requires the *_test
// database. The DB also enforces feature<>'' and operation IN (...) via CHECK.
func TestCommandFeatureCoverage(t *testing.T) {
t.Parallel()
cat := loadRealCatalog(t)
validOp := map[string]bool{"read": true, "write": true, "delete": true, "action": true}
for _, c := range cat {
@@ -172,22 +172,7 @@ func TestCommandFeatureCoverage(t *testing.T) {
// skipping the test when the DB isn't configured.
func loadRealCatalog(t *testing.T) []*store.Command {
t.Helper()
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run this integration test")
}
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: test DB name must end in _test (got %q)", dsn)
}
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
t.Fatalf("store: %v", err)
}
t.Cleanup(st.Close)
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
st, ctx := coreStore(t)
cat, err := st.ListCommands(ctx)
if err != nil {
t.Fatalf("list commands: %v", err)
@@ -200,6 +185,7 @@ func loadRealCatalog(t *testing.T) []*store.Command {
// Requires the *_test database (it runs migrations).
func TestResolveCommandRealCatalog(t *testing.T) {
t.Parallel()
cat := loadRealCatalog(t)
argGroup := regexp.MustCompile(`<[^>]*>`)
seen := map[string]string{} // (token|arity) -> key
+47
View File
@@ -0,0 +1,47 @@
package core
import (
"context"
"os"
"testing"
"time"
"github.com/jleight/meshtender/internal/store"
"github.com/jleight/meshtender/internal/testdb"
)
// coreStore returns a Store backed by a fresh, throwaway database cloned from
// the migrated template (see internal/testdb). Each call is fully isolated —
// command_catalog seeded, everything else empty — so the integration tests need
// no truncation and don't share state.
func coreStore(t *testing.T) (*store.Store, context.Context) {
t.Helper()
ctx := context.Background()
st, err := store.New(ctx, testdb.Fresh(t, coreMigrate))
if err != nil {
t.Fatalf("store: %v", err)
}
t.Cleanup(st.Close)
return st, ctx
}
// coreMigrate applies the schema to the template database, releasing its
// connection before the template is cloned.
func coreMigrate(dsn string) error {
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
return err
}
defer st.Close()
return st.Migrate(ctx)
}
// TestMain wires process-level setup/teardown for the testdb template/container.
// It also shortens the packet-reply wait once, before any test runs: the
// integration tests run in parallel and only ever read perTryReply, so setting
// it here (rather than mutating it per-test) keeps it race-free under -race.
func TestMain(m *testing.M) {
perTryReply = 300 * time.Millisecond
os.Exit(testdb.RunMain(m))
}
+8 -21
View File
@@ -6,7 +6,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
@@ -30,26 +29,7 @@ type hostEnv struct{ auth, app, root, www string }
// against the test database, returning the store and the per-surface host:port.
func splitServer(t *testing.T) (*store.Store, context.Context, *httptest.Server, hostEnv) {
t.Helper()
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run this integration test")
}
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: test DB name must end in _test (got %q)", dsn)
}
ctx := context.Background()
st, err := store.New(ctx, dsn)
if err != nil {
t.Fatalf("store: %v", err)
}
t.Cleanup(st.Close)
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
if _, err := st.Pool().Exec(ctx,
`TRUNCATE users, repeaters, server_identity, sessions, auth_codes RESTART IDENTITY CASCADE`); err != nil {
t.Fatalf("truncate: %v", err)
}
st, ctx := coreStore(t)
var masterKey [32]byte
_, _ = rand.Read(masterKey[:])
@@ -158,6 +138,7 @@ func cookieByName(resp *http.Response, name string) *http.Cookie {
// protected page redirects to the auth host's /login, carrying next and a state
// nonce that matches the host-only state cookie just set.
func TestRequireUserBouncesToAuthHost(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
resp := do(t, ts, h.app, "/repeaters")
@@ -182,6 +163,7 @@ func TestRequireUserBouncesToAuthHost(t *testing.T) {
// TestAppHostLoginRedirectsToAuth: the credential UI does not live on the app
// host; /login there bounces to the auth host.
func TestAppHostLoginRedirectsToAuth(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
resp := do(t, ts, h.app, "/login")
defer resp.Body.Close()
@@ -195,6 +177,7 @@ func TestAppHostLoginRedirectsToAuth(t *testing.T) {
// TestAuthHostServesLogin: the auth host renders the sign-in page in place.
func TestAuthHostServesLogin(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
resp := do(t, ts, h.auth, "/login")
defer resp.Body.Close()
@@ -207,6 +190,7 @@ func TestAuthHostServesLogin(t *testing.T) {
// serves public marketing + org discovery, www redirects to root, and the app
// host's / and /orgs require auth (bounce to the auth host).
func TestHostRouting(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
t.Run("root serves landing", func(t *testing.T) {
@@ -256,6 +240,7 @@ func TestHostRouting(t *testing.T) {
// /logout chains to the auth host, and the auth host's /logout destroys the SSO
// session so it can't silently re-authenticate.
func TestSingleLogout(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
// An app session (via the handoff) logs out by bouncing to the auth host.
@@ -303,6 +288,7 @@ func TestSingleLogout(t *testing.T) {
// SSO session, and an SSO-less visit returns LOCALLY to /account after login
// (rather than handing off to the app).
func TestAccountOnAuthHost(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
t.Run("app host no longer serves /account", func(t *testing.T) {
@@ -354,6 +340,7 @@ func TestAccountOnAuthHost(t *testing.T) {
// state establishes an app-host session; tampered state or an unknown code is
// rejected back to sign-in without a session.
func TestSessionCallback(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
appHost := h.app
u, err := st.CreateUser(ctx, "callbackuser", "")
+3
View File
@@ -7,6 +7,7 @@ import (
)
func TestNormalizeHostname(t *testing.T) {
t.Parallel()
cases := map[string]string{
"mesh.example.org": "mesh.example.org",
" Mesh.Example.ORG ": "mesh.example.org",
@@ -28,6 +29,7 @@ func TestNormalizeHostname(t *testing.T) {
}
func TestTxtRecordsHaveToken(t *testing.T) {
t.Parallel()
tok := "secret-token"
if !txtRecordsHaveToken([]string{"other", " secret-token "}, tok) {
t.Error("should match token with surrounding whitespace among other records")
@@ -41,6 +43,7 @@ func TestTxtRecordsHaveToken(t *testing.T) {
}
func TestHostWithoutPort(t *testing.T) {
t.Parallel()
cases := map[string]string{
"example.org:8080": "example.org",
"example.org": "example.org",
+1
View File
@@ -14,6 +14,7 @@ import (
// or template-resolution error. This guards the startup-time template
// composition (each page redefines content/title/header onto the base set).
func TestBuildPagesComposeAndExecute(t *testing.T) {
t.Parallel()
rn, err := web.NewRenderer(&config.Config{}, templatesFS)
if err != nil {
t.Fatalf("NewRenderer: %v", err)
+3
View File
@@ -7,6 +7,7 @@ import (
)
func TestSealOpenRoundTrip(t *testing.T) {
t.Parallel()
var key [32]byte
if _, err := rand.Read(key[:]); err != nil {
t.Fatal(err)
@@ -31,6 +32,7 @@ func TestSealOpenRoundTrip(t *testing.T) {
}
func TestOpenWrongKeyFails(t *testing.T) {
t.Parallel()
var key, wrong [32]byte
_, _ = rand.Read(key[:])
_, _ = rand.Read(wrong[:])
@@ -45,6 +47,7 @@ func TestOpenWrongKeyFails(t *testing.T) {
}
func TestOpenTooShort(t *testing.T) {
t.Parallel()
var key [32]byte
_, _ = rand.Read(key[:])
if _, err := open(key, []byte{0x01, 0x02}); err == nil {
+2
View File
@@ -3,6 +3,7 @@ package marketing
import "testing"
func TestOrgCursorRoundTrip(t *testing.T) {
t.Parallel()
cases := []orgCursor{
{Sort: "name", Name: "Acme Mesh", ID: 1},
{Sort: "members", Count: 0, ID: 0},
@@ -20,6 +21,7 @@ func TestOrgCursorRoundTrip(t *testing.T) {
}
func TestDecodeOrgCursorMalformed(t *testing.T) {
t.Parallel()
// A missing or garbage cursor must decode to ok=false, never an error, so a
// tampered URL just resets paging rather than 500ing.
for _, tok := range []string{"", "not-base64!!", "Zm9v" /* "foo", not JSON */} {
+2
View File
@@ -9,6 +9,7 @@ import (
)
func TestBuildCommandPacketDecodableByRepeater(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
now := time.Unix(1_700_001_000, 0)
@@ -55,6 +56,7 @@ func TestBuildCommandPacketDecodableByRepeater(t *testing.T) {
}
func TestDecodeCommandReply(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
now := time.Unix(1_700_001_500, 0)
+5
View File
@@ -38,6 +38,7 @@ func (f *fakeModem) SendData(_ []byte) error {
}
func TestExchangerRetriesThenSucceeds(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
fm := &fakeModem{repeater: repeater, server: server.Identity, replyText: "> 42.0", dropN: 2}
@@ -61,6 +62,7 @@ func TestExchangerRetriesThenSucceeds(t *testing.T) {
}
func TestExchangerExhaustsRetries(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
fm := &fakeModem{repeater: repeater, server: server.Identity, replyText: "x", dropN: 100}
@@ -90,6 +92,7 @@ func (f *routeFakeModem) SendData(raw []byte) error {
// TestExchangerUsesDirectAfterLogin verifies that once Login learns a path, the
// next Command is sent via direct routing carrying that exact path.
func TestExchangerUsesDirectAfterLogin(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
shared, _ := repeater.SharedSecret(server.Identity)
@@ -177,6 +180,7 @@ func (f *seqModem) SendData(_ []byte) error {
// reply it rejects and keeps waiting for one it accepts — the guard that keeps
// a straggling "get lat" duplicate from being misread as the longitude.
func TestCommandAcceptRejectsStaleReply(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
// First two sends echo the stale latitude; the third gives the longitude.
@@ -199,6 +203,7 @@ func TestCommandAcceptRejectsStaleReply(t *testing.T) {
}
func TestExchangerCancelled(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
fm := &fakeModem{repeater: repeater, server: server.Identity, dropN: 100}
+6
View File
@@ -66,6 +66,7 @@ func buildRepeaterResponse(t *testing.T, repeater meshcore.LocalIdentity, server
}
func TestLoginRequestDecodableByRepeater(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
now := time.Unix(1_700_000_000, 0)
@@ -169,6 +170,7 @@ func buildRepeaterPathReply(t *testing.T, repeater meshcore.LocalIdentity, serve
}
func TestLoginPathReplyRoundTrip(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
now := time.Unix(1_700_000_900, 0).UTC()
@@ -192,6 +194,7 @@ func TestLoginPathReplyRoundTrip(t *testing.T) {
// from a live repeater (admin granted via `setperm <pubkey> 3`). It guards the
// timestamp-anchored field offsets against regressions.
func TestParseRealPathReply(t *testing.T) {
t.Parallel()
// raw = [path_len=00][tag=01][ts:4 LE = 0x6a34b5e3][code=00][rsv=00]
// [admin=01][perms=03][random:4][fw=02][pad=00]
plain, err := hex.DecodeString("0001e3b5346a000001030b9213f80200")
@@ -221,6 +224,7 @@ func TestParseRealPathReply(t *testing.T) {
}
func TestLoginResponseRoundTrip(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
now := time.Unix(1_700_000_500, 0).UTC()
@@ -246,6 +250,7 @@ func TestLoginResponseRoundTrip(t *testing.T) {
}
func TestDecodeLoginResponseWrongRepeater(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
other := mustIdentity(t)
@@ -261,6 +266,7 @@ func TestDecodeLoginResponseWrongRepeater(t *testing.T) {
}
func TestDecodeLoginResponseNotResponse(t *testing.T) {
t.Parallel()
server := mustIdentity(t)
repeater := mustIdentity(t)
+1
View File
@@ -3,6 +3,7 @@ package mesh
import "testing"
func TestSeqClockStrictlyIncreasing(t *testing.T) {
t.Parallel()
c := &SeqClock{}
prev := int64(0)
for i := 0; i < 100; i++ { // tight loop → same wall-clock second
+1
View File
@@ -3,6 +3,7 @@ package store
import "testing"
func TestAuthCodes(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
u, err := st.CreateUser(ctx, "codeuser", "")
if err != nil {
@@ -10,6 +10,7 @@ import (
// checks every session with commands appears once, newest-first, across pages —
// and that a session with no commands is omitted.
func TestListCommandLogSessionsPage(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
owner, err := st.CreateUser(ctx, "logowner", "")
+4
View File
@@ -7,6 +7,7 @@ import (
)
func TestRepeaterPublicID(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
owner, _ := st.CreateUser(ctx, "pidowner", "")
@@ -39,6 +40,7 @@ func TestRepeaterPublicID(t *testing.T) {
}
func TestOrgSlugGeneration(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
creator, _ := st.CreateUser(ctx, "slugcreator", "")
@@ -76,6 +78,7 @@ func TestOrgSlugGeneration(t *testing.T) {
}
func TestValidOrgSlug(t *testing.T) {
t.Parallel()
cases := map[string]bool{
"buffalo-mesh": true,
"abc": true,
@@ -97,6 +100,7 @@ func TestValidOrgSlug(t *testing.T) {
}
func TestOrgDomains(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
creator, _ := st.CreateUser(ctx, "domowner", "")
org, err := st.CreateOrg(ctx, "Domain Org", creator.ID)
+14
View File
@@ -0,0 +1,14 @@
package store
import (
"os"
"testing"
"github.com/jleight/meshtender/internal/testdb"
)
// TestMain gives the package's DB-backed tests process-level setup/teardown of
// the testdb template and (when used) its container.
func TestMain(m *testing.M) {
os.Exit(testdb.RunMain(m))
}
+3
View File
@@ -8,6 +8,7 @@ import (
// TestListPublicOrgsPageByName walks the name-sorted directory and checks every
// org appears exactly once, in (name, id) order, across pages.
func TestListPublicOrgsPageByName(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
owner, err := st.CreateUser(ctx, "pager", "")
@@ -68,6 +69,7 @@ func TestListPublicOrgsPageByName(t *testing.T) {
// TestListPublicOrgsPageByMembers checks the default ordering puts orgs with the
// most members first, and that the count-keyset seek pages without dupes.
func TestListPublicOrgsPageByMembers(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
// Give each org a distinct member count: org i ends up with i+1 members
@@ -113,6 +115,7 @@ func TestListPublicOrgsPageByMembers(t *testing.T) {
// TestListPublicOrgsPageSearch checks the search term matches name, description,
// and region case-insensitively, and that wildcards are treated literally.
func TestListPublicOrgsPageSearch(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
owner, err := st.CreateUser(ctx, "searcher", "")
+22 -20
View File
@@ -3,41 +3,42 @@ package store
import (
"context"
"errors"
"net/url"
"os"
"strings"
"testing"
"github.com/jleight/meshtender/internal/testdb"
)
// orgTestStore opens the test DB (gated, *_test only) and wipes mutable state,
// preserving the migration-seeded command_catalog.
// orgTestStore returns a Store backed by a fresh, throwaway database cloned from
// the migrated template (see internal/testdb). Each call gets pristine state —
// command_catalog seeded, everything else empty — so tests need no truncation
// and can run in parallel.
func orgTestStore(t *testing.T) (*Store, context.Context) {
t.Helper()
dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set MESHTENDER_TEST_DATABASE_URL to run org resolution tests")
}
if u, err := url.Parse(dsn); err != nil || !strings.HasSuffix(strings.TrimPrefix(u.Path, "/"), "_test") {
t.Fatalf("refusing to run: test DB name must end in _test (got %q)", dsn)
}
ctx := context.Background()
st, err := New(ctx, dsn)
st, err := New(ctx, testdb.Fresh(t, migrateTemplate))
if err != nil {
t.Fatalf("store: %v", err)
}
t.Cleanup(st.Close)
if err := st.Migrate(ctx); err != nil {
t.Fatalf("migrate: %v", err)
}
// Wipe state but keep command_catalog (seeded by migration).
if _, err := st.pool.Exec(ctx,
`TRUNCATE users, repeaters, organizations RESTART IDENTITY CASCADE`); err != nil {
t.Fatalf("truncate: %v", err)
}
return st, ctx
}
// migrateTemplate applies the schema to the template database. It opens its own
// store and closes it before returning, so no connection lingers on the
// template when it's cloned.
func migrateTemplate(dsn string) error {
ctx := context.Background()
st, err := New(ctx, dsn)
if err != nil {
return err
}
defer st.Close()
return st.Migrate(ctx)
}
func TestOrgCommandResolution(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
cmdID := func(key string) int64 {
@@ -138,6 +139,7 @@ func TestOrgCommandResolution(t *testing.T) {
}
func TestOrgRepeaterAccess(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
owner, _ := st.CreateUser(ctx, "owner2", "")
member, _ := st.CreateUser(ctx, "member2", "")
+1
View File
@@ -11,6 +11,7 @@ func selfChange(uid int64) UsernameChangeContext {
}
func TestSetUsername(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t)
mk := func(name string) int64 {
+1
View File
@@ -8,6 +8,7 @@ import (
// TestListUsersPage walks the keyset-paginated admin user list and checks every
// user appears exactly once, in username order, across pages.
func TestListUsersPage(t *testing.T) {
t.Parallel()
st, ctx := orgTestStore(t) // reuses the gated *_test store + truncation
total := UsersPageSize*2 + 3
+221
View File
@@ -0,0 +1,221 @@
// Package testdb gives each test its own throwaway Postgres database.
//
// The model (borrowed from a pattern common in .NET test suites): once per test
// process we obtain a Postgres server, build a single migrated "template"
// database, and then hand every test a fresh database cloned from that template
// via CREATE DATABASE ... TEMPLATE. Cloning copies the already-migrated schema
// and seed data, so individual tests never run migrations and never share
// state — no TRUNCATE, no ordering constraints, safe to run in parallel.
//
// The server comes from one of two places:
//
// - If MESHTENDER_TEST_DATABASE_URL is set, that server is reused (this is how
// CI points at its postgres service container).
// - Otherwise a postgres:17 container is started via testcontainers, so a
// local `go test ./...` just works as long as Docker is running.
//
// This package deliberately does not import internal/store: store's own
// (internal) tests import testdb, and the reverse dependency would be a cycle.
// Migrations are injected by the caller through the migrate callback.
package testdb
import (
"context"
"fmt"
"net/url"
"os"
"sync"
"sync/atomic"
"testing"
"github.com/jackc/pgx/v5"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
// pid disambiguates database names across the separate test binaries (one per
// package) that may share a single reused server in CI.
var pid = os.Getpid()
var (
serverOnce sync.Once
serverErr error
adminDSN string // points at a maintenance DB on the server
container *postgres.PostgresContainer // nil when reusing an external server
templateOnce sync.Once
templateErr error
templateName string
createMu sync.Mutex // serializes per-test CREATE DATABASE within this process
dbCounter atomic.Int64
)
// templateLockKey is an arbitrary, fixed advisory-lock id. It serializes
// template creation across the package test binaries that share one server (CI),
// where they would otherwise race to CREATE DATABASE from template1.
const templateLockKey int64 = 0x4d5465737444_42
// ensureServer resolves a Postgres server exactly once per process: a reused
// external one via MESHTENDER_TEST_DATABASE_URL, or a fresh container.
func ensureServer(ctx context.Context) error {
serverOnce.Do(func() {
if dsn := os.Getenv("MESHTENDER_TEST_DATABASE_URL"); dsn != "" {
adminDSN = dsn
return
}
c, err := postgres.Run(ctx, "postgres:17",
postgres.WithDatabase("postgres"),
postgres.WithUsername("meshtender"),
postgres.WithPassword("meshtender"),
postgres.BasicWaitStrategies(),
)
if err != nil {
serverErr = fmt.Errorf("start postgres container (is Docker running?): %w", err)
return
}
container = c
dsn, err := c.ConnectionString(ctx, "sslmode=disable")
if err != nil {
serverErr = fmt.Errorf("container connection string: %w", err)
return
}
adminDSN = dsn
})
return serverErr
}
// ensureTemplate creates and migrates the per-process template database exactly
// once. migrate is invoked with the template's DSN and must apply the schema and
// then release all its connections (the subsequent CREATE DATABASE ... TEMPLATE
// requires that no sessions are connected to the template).
func ensureTemplate(ctx context.Context, migrate func(dsn string) error) error {
templateOnce.Do(func() {
templateName = fmt.Sprintf("mt_tmpl_%d", pid)
conn, err := pgx.Connect(ctx, adminDSN)
if err != nil {
templateErr = fmt.Errorf("admin connect: %w", err)
return
}
defer conn.Close(ctx)
// Hold a cross-process advisory lock only around the template DDL: this is
// the one CREATE DATABASE that copies the shared template1, so concurrent
// package binaries on one server must take turns. Released before migrate,
// which runs on its own connection against the new template.
if _, err := conn.Exec(ctx, `SELECT pg_advisory_lock($1)`, templateLockKey); err != nil {
templateErr = fmt.Errorf("advisory lock: %w", err)
return
}
_, dropErr := conn.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(templateName)+` WITH (FORCE)`)
var createErr error
if dropErr == nil {
_, createErr = conn.Exec(ctx, `CREATE DATABASE `+quoteIdent(templateName))
}
if _, err := conn.Exec(ctx, `SELECT pg_advisory_unlock($1)`, templateLockKey); err != nil {
templateErr = fmt.Errorf("advisory unlock: %w", err)
return
}
if dropErr != nil {
templateErr = fmt.Errorf("drop stale template: %w", dropErr)
return
}
if createErr != nil {
templateErr = fmt.Errorf("create template: %w", createErr)
return
}
tmplDSN, err := dsnWithDB(adminDSN, templateName)
if err != nil {
templateErr = err
return
}
if err := migrate(tmplDSN); err != nil {
templateErr = fmt.Errorf("migrate template: %w", err)
}
})
return templateErr
}
// Fresh provisions a brand-new database cloned from the migrated template and
// returns its DSN. The database is dropped when the test finishes. migrate is
// used only to build the template the first time it's called in this process;
// later calls reuse the existing template.
func Fresh(t *testing.T, migrate func(dsn string) error) string {
t.Helper()
ctx := context.Background()
if err := ensureServer(ctx); err != nil {
t.Fatalf("testdb: %v", err)
}
if err := ensureTemplate(ctx, migrate); err != nil {
t.Fatalf("testdb: %v", err)
}
name := fmt.Sprintf("mt_test_%d_%d", pid, dbCounter.Add(1))
createMu.Lock()
conn, err := pgx.Connect(ctx, adminDSN)
if err == nil {
_, err = conn.Exec(ctx, `CREATE DATABASE `+quoteIdent(name)+` TEMPLATE `+quoteIdent(templateName))
conn.Close(ctx)
}
createMu.Unlock()
if err != nil {
t.Fatalf("testdb: create database: %v", err)
}
t.Cleanup(func() {
c, err := pgx.Connect(ctx, adminDSN)
if err != nil {
return
}
defer c.Close(ctx)
_, _ = c.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(name)+` WITH (FORCE)`)
})
dsn, err := dsnWithDB(adminDSN, name)
if err != nil {
t.Fatalf("testdb: %v", err)
}
return dsn
}
// RunMain wraps a package's tests with process-level teardown: it drops the
// template and, when we started one, terminates the container. Use it from a
// package's TestMain:
//
// func TestMain(m *testing.M) { os.Exit(testdb.RunMain(m)) }
func RunMain(m *testing.M) int {
code := m.Run()
ctx := context.Background()
if templateName != "" && adminDSN != "" {
if conn, err := pgx.Connect(ctx, adminDSN); err == nil {
_, _ = conn.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(templateName)+` WITH (FORCE)`)
conn.Close(ctx)
}
}
if container != nil {
_ = container.Terminate(ctx)
}
return code
}
// dsnWithDB returns base with its database (path) replaced by dbName and a small
// pool cap applied. The cap matters under parallel tests: many tests each open a
// pool against the same server, so an uncapped default would exhaust Postgres's
// connection limit. These DSNs are only ever handed to pgxpool (store.New), so
// the pool_* parameter is safe here.
func dsnWithDB(base, dbName string) (string, error) {
u, err := url.Parse(base)
if err != nil {
return "", fmt.Errorf("parse dsn: %w", err)
}
u.Path = "/" + dbName
q := u.Query()
q.Set("pool_max_conns", "4")
u.RawQuery = q.Encode()
return u.String(), nil
}
// quoteIdent double-quotes a generated identifier. Names here are built from a
// fixed prefix plus integers, so this is just correctness, not injection
// defense.
func quoteIdent(name string) string {
return `"` + name + `"`
}
+2
View File
@@ -6,6 +6,7 @@ import (
)
func TestRateLimiterBurstThenThrottle(t *testing.T) {
t.Parallel()
now := time.Unix(0, 0)
l := NewRateLimiter(3, time.Second)
l.now = func() time.Time { return now }
@@ -35,6 +36,7 @@ func TestRateLimiterBurstThenThrottle(t *testing.T) {
}
func TestRateLimiterSweepReclaims(t *testing.T) {
t.Parallel()
now := time.Unix(0, 0)
l := NewRateLimiter(2, time.Second)
l.now = func() time.Time { return now }
+1
View File
@@ -12,6 +12,7 @@ import (
// (a split immediately after a frame's trailing FEND) that made the SDK's
// ExtractFrames silently drop the following frame.
func TestFeedReassemblesAcrossChunkBoundaries(t *testing.T) {
t.Parallel()
// Three back-to-back frames: a data frame and two hardware frames
// (TX_DONE, RX_META) — exactly the mix a modem emits around a transmit.
f1 := hardware.EncodeDataFrame([]byte{0x21, 0x00, 0x3d, 0x37, 0xaa, 0xbb, 0xcc})