aboutsummaryrefslogtreecommitdiff
path: root/cmd/mobius-hotline-server
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-10 09:48:18 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-10 09:48:18 -0700
commit21f24d24fd6f501b32f15a2bef41c89cc461f623 (patch)
treeba4952fb82f3ef9c265228633f27536866e23931 /cmd/mobius-hotline-server
parentae44fb222ec73cae8441f5a5d9a21f8585fe587f (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/mobius-hotline-server')
-rw-r--r--cmd/mobius-hotline-server/main_test.go80
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)
+ })
+ }
+}