diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-10 09:48:18 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-10 09:48:18 -0700 |
| commit | 21f24d24fd6f501b32f15a2bef41c89cc461f623 (patch) | |
| tree | ba4952fb82f3ef9c265228633f27536866e23931 /cmd | |
| parent | ae44fb222ec73cae8441f5a5d9a21f8585fe587f (diff) | |
Overhaul regression testing: e2e suite, fuzzing, CI, and bug fixes
Add a protocol-level end-to-end suite (in-process fully wired server on
an ephemeral port pair, driven by hotline.Client over TCP) covering
handshake, login, public and private chat, message board, threaded
news, file list/download/upload, account admin, disconnect
notification, and shutdown broadcast. The harness retries on a fresh
port pair when another process steals a probed port before
ListenAndServe binds it, and Server gains WithConnectionRateLimit so
tests can disable the per-IP connection throttle.
Add native fuzz tests for Transaction, Field, and flattened file
object decoding, and fix the bugs the new tests surfaced:
- Transaction.Write panicked on out-of-range attacker-controlled size
fields, and transactionScanner's uint32 length addition could wrap
and yield a truncated token. The information fork size declared in
an untrusted fork header is now bounded too.
- Client keepalive read c.done unsynchronized while Disconnect
replaces it under the mutex.
- The shared Agreement's Seek+ReadAll login path raced concurrent
logins; the server now prefers an AgreementBytes() snapshot.
Fill unit-test gaps (main's config-copy helpers, file resume data,
ReloaderFunc, R2 error injection and env validation) and add a CI test
workflow (build/vet + race-enabled shuffled suite), fixed lint
workflow triggers with golangci-lint v2.6, and Makefile test/cover/
lint/fuzz targets.
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/mobius-hotline-server/main_test.go | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/cmd/mobius-hotline-server/main_test.go b/cmd/mobius-hotline-server/main_test.go index a527d8b..827397a 100644 --- a/cmd/mobius-hotline-server/main_test.go +++ b/cmd/mobius-hotline-server/main_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "path" "testing" @@ -179,3 +180,82 @@ func TestFindConfigPath(t *testing.T) { assert.Equal(t, "config", result) }) } + +// r2EnvVars are every R2_* variable newR2FileStore reads. Each case clears all of them and sets +// only what it needs, so ambient credentials in the test environment can't leak in. +var r2EnvVars = []string{ + "R2_BUCKET", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY", + "R2_ENDPOINT", "R2_ACCOUNT_ID", "R2_PREFIX", "R2_STAGING_DIR", +} + +func TestNewR2FileStore_EnvValidation(t *testing.T) { + tests := []struct { + name string + env map[string]string + wantErr string // substring; empty means the store must construct successfully + }{ + { + name: "missing bucket and credentials", + env: map[string]string{}, + wantErr: "R2_BUCKET, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY must be set", + }, + { + name: "missing secret key", + env: map[string]string{ + "R2_BUCKET": "b", + "R2_ACCESS_KEY_ID": "ak", + }, + wantErr: "R2_BUCKET, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY must be set", + }, + { + name: "credentials set but no endpoint or account id", + env: map[string]string{ + "R2_BUCKET": "b", + "R2_ACCESS_KEY_ID": "ak", + "R2_SECRET_ACCESS_KEY": "sk", + }, + wantErr: "either R2_ENDPOINT or R2_ACCOUNT_ID must be set", + }, + { + name: "account id derives the endpoint", + env: map[string]string{ + "R2_BUCKET": "b", + "R2_ACCESS_KEY_ID": "ak", + "R2_SECRET_ACCESS_KEY": "sk", + "R2_ACCOUNT_ID": "acct123", + }, + }, + { + name: "explicit endpoint", + env: map[string]string{ + "R2_BUCKET": "b", + "R2_ACCESS_KEY_ID": "ak", + "R2_SECRET_ACCESS_KEY": "sk", + "R2_ENDPOINT": "https://example.com", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, k := range r2EnvVars { + t.Setenv(k, "") + } + // Keep staging off the shared temp dir even on the success paths. + t.Setenv("R2_STAGING_DIR", t.TempDir()) + for k, v := range tt.env { + t.Setenv(k, v) + } + + store, err := newR2FileStore(context.Background()) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + assert.Nil(t, store) + return + } + require.NoError(t, err) + assert.NotNil(t, store) + }) + } +} |