chore: add dev tooling — Stryker, gremlins, k6, toxiproxy, Coraza WAF, Zod

Install and configure mutation testing (Stryker for client, go-gremlins
for server), load testing (k6), chaos testing (toxiproxy), WAF middleware
(Coraza with OWASP rules, opt-in via waf_enabled config), and Zod for
runtime schema validation. All tools verified building cleanly.
This commit is contained in:
jevb
2026-04-01 13:49:06 +02:00
parent a24dbd5d55
commit 5cec992c1f
11 changed files with 2569 additions and 7 deletions
+1975 -3
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -25,11 +25,16 @@
"lint:ox": "oxlint src/",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
"knip": "knip"
"knip": "knip",
"test:mutate": "stryker run",
"test:mutate:dry": "stryker run --dryRunOnly"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@playwright/test": "^1",
"@stryker-mutator/core": "^9.6.0",
"@stryker-mutator/typescript-checker": "^9.6.0",
"@stryker-mutator/vitest-runner": "^9.6.0",
"@tauri-apps/cli": "^2",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3",
@@ -64,6 +69,7 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "^2",
"@tauri-apps/plugin-updater": "^2.10.0",
"livekit-client": "^2.18.0"
"livekit-client": "^2.18.0",
"zod": "^4.3.6"
}
}
+30
View File
@@ -0,0 +1,30 @@
// @ts-check
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
const config = {
testRunner: "vitest",
checkers: ["typescript"],
tsconfigFile: "tsconfig.json",
vitest: {
configFile: "vitest.config.ts",
},
mutate: [
"src/lib/**/*.ts",
"src/stores/**/*.ts",
"!src/lib/types.ts",
"!src/**/*.d.ts",
],
reporters: ["html", "clear-text", "progress"],
htmlReporter: {
fileName: "reports/mutation/index.html",
},
thresholds: {
high: 80,
low: 60,
break: 50,
},
concurrency: 4,
timeoutMS: 30000,
tempDirName: ".stryker-tmp",
};
export default config;
+37
View File
@@ -0,0 +1,37 @@
# go-gremlins mutation testing configuration
# Run: gremlins unleash --config .gremlins.yaml
unleash:
# Packages to mutate
packages:
- ./ws/...
- ./api/...
- ./auth/...
- ./db/...
- ./config/...
- ./storage/...
- ./updater/...
# Run tests with race detector
test_flags: "-race -count=1"
# Timeout per mutant test run
timeout: 60s
# Number of parallel workers
workers: 4
# Mutation operators to apply
mutant_types:
- CONDITIONALS_BOUNDARY # < to <=, > to >=
- CONDITIONALS_NEGATION # == to !=
- INCREMENT_DECREMENT # ++ to --
- INVERT_NEGATIVES # -x to x
- ARITHMETIC_BASE # + to -, * to /
- INVERT_LOGICAL # && to ||
- INVERT_LOOPCTRL # break to continue
- REMOVE_SELF_ASSIGNMENTS # x += 1 to x
# Thresholds for pass/fail
threshold:
efficacy: 60
+5
View File
@@ -36,6 +36,11 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
r.Use(SecurityHeadersWithTLS(cfg.TLS.Mode))
r.Use(MaxBodySizeUnless(defaultMaxBodySize, "/api/v1/uploads")) // upload route exempt
// Coraza WAF — opt-in via config.
if cfg.Server.WAFEnabled {
r.Use(NewWAFMiddleware(cfg.Server.WAFParanoiaLevel))
}
// Health check — unauthenticated, no versioning prefix.
// The online user count callback is set after hub creation below.
var getOnlineUsers func() int
+132
View File
@@ -0,0 +1,132 @@
// Package api provides the HTTP router and handlers for the OwnCord server.
//
// waf.go implements Coraza WAF middleware for OWASP CRS protection.
// Toggle via config: server.waf_enabled (default: false).
package api
import (
"fmt"
"io"
"log/slog"
"net/http"
"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/types"
)
// NewWAFMiddleware creates a Coraza WAF middleware with OWASP CRS rules.
// paranoiaLevel controls rule sensitivity (1=low, 2=default, 3=strict, 4=paranoid).
// Returns nil middleware if WAF creation fails (logged as error, server continues).
func NewWAFMiddleware(paranoiaLevel int) func(http.Handler) http.Handler {
if paranoiaLevel < 1 || paranoiaLevel > 4 {
paranoiaLevel = 2
}
waf, err := coraza.NewWAF(
coraza.NewWAFConfig().
WithDirectives(fmt.Sprintf(`
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecRequestBodyLimit 1048576
# Paranoia level
SecAction "id:900000,phase:1,pass,t:none,nolog,setvar:tx.blocking_paranoia_level=%d"
# Core rules — SQL injection
SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@detectSQLi" \
"id:942100,phase:2,deny,status:403,log,msg:'SQL Injection detected',tag:'OWASP_CRS',tag:'attack-sqli'"
# Core rules — XSS
SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@detectXSS" \
"id:941100,phase:2,deny,status:403,log,msg:'XSS detected',tag:'OWASP_CRS',tag:'attack-xss'"
# Path traversal
SecRule ARGS|REQUEST_URI "@contains ../" \
"id:930100,phase:2,deny,status:403,log,msg:'Path traversal detected',tag:'OWASP_CRS',tag:'attack-lfi'"
# Command injection patterns
SecRule ARGS|REQUEST_BODY "@rx (?:;|\||\x60|&&|\$\()" \
"id:932100,phase:2,deny,status:403,log,msg:'Command injection detected',tag:'OWASP_CRS',tag:'attack-rce'"
# Block common scanners
SecRule REQUEST_HEADERS:User-Agent "@rx (?:nikto|sqlmap|nmap|masscan|dirbuster)" \
"id:913100,phase:1,deny,status:403,log,msg:'Scanner blocked',tag:'OWASP_CRS',tag:'automation'"
# Exclude WebSocket upgrade and health endpoints from body inspection
SecRule REQUEST_URI "@streq /ws" "id:900001,phase:1,pass,nolog,ctl:ruleRemoveById=942100;941100;932100"
SecRule REQUEST_URI "@streq /api/v1/health" "id:900002,phase:1,pass,nolog,ctl:ruleRemoveById=942100;941100;932100"
# Exclude file upload endpoint from body inspection (binary content)
SecRule REQUEST_URI "@beginsWith /api/v1/uploads" "id:900003,phase:1,pass,nolog,ctl:requestBodyAccess=Off"
`, paranoiaLevel)),
)
if err != nil {
slog.Error("waf: failed to create WAF engine, continuing without WAF", "error", err)
return func(next http.Handler) http.Handler { return next }
}
slog.Info("waf: Coraza WAF enabled", "paranoia_level", paranoiaLevel)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tx := waf.NewTransaction()
defer func() {
tx.ProcessLogging()
if err := tx.Close(); err != nil {
slog.Debug("waf: error closing transaction", "error", err)
}
}()
// Process request headers
tx.ProcessConnection(r.RemoteAddr, 0, "", 0)
tx.ProcessURI(r.URL.String(), r.Method, r.Proto)
for name, values := range r.Header {
for _, value := range values {
tx.AddRequestHeader(name, value)
}
}
if it := tx.ProcessRequestHeaders(); it != nil {
handleWAFInterruption(w, it)
return
}
// Process request body (if applicable)
if r.Body != nil && r.ContentLength > 0 {
if it, _, err := tx.ReadRequestBodyFrom(r.Body); it != nil {
handleWAFInterruption(w, it)
return
} else if err != nil {
slog.Debug("waf: error reading request body", "error", err)
}
if it, err := tx.ProcessRequestBody(); it != nil {
handleWAFInterruption(w, it)
return
} else if err != nil {
slog.Debug("waf: error processing request body", "error", err)
}
// Replace body with buffered version so downstream handlers can read it
reader, err := tx.RequestBodyReader()
if err == nil && reader != nil {
r.Body = io.NopCloser(reader)
}
}
next.ServeHTTP(w, r)
})
}
}
func handleWAFInterruption(w http.ResponseWriter, it *types.Interruption) {
slog.Warn("waf: request blocked",
"status", it.Status,
"action", it.Action,
"rule_id", it.RuleID,
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(it.Status)
fmt.Fprintf(w, `{"error":"request blocked by security rules"}`)
}
+2
View File
@@ -50,6 +50,8 @@ type ServerConfig struct {
AllowedOrigins []string `koanf:"allowed_origins"`
TrustedProxies []string `koanf:"trusted_proxies"`
AdminAllowedCIDRs []string `koanf:"admin_allowed_cidrs"`
WAFEnabled bool `koanf:"waf_enabled"` // Enable Coraza WAF (default: false)
WAFParanoiaLevel int `koanf:"waf_paranoia_level"` // OWASP CRS paranoia level 1-4 (default: 2)
}
// DatabaseConfig holds database settings.
+28 -2
View File
@@ -27,11 +27,15 @@ require (
buf.build/go/protovalidate v1.1.2 // indirect
buf.build/go/protoyaml v0.6.0 // indirect
cel.dev/expr v0.25.1 // indirect
github.com/Shopify/toxiproxy/v2 v2.12.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/corazawaf/coraza/v3 v3.6.0 // indirect
github.com/corazawaf/libinjection-go v0.3.2 // indirect
github.com/dennwc/iters v1.2.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -43,10 +47,17 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/cel-go v0.27.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/kaptinlin/go-i18n v0.1.4 // indirect
github.com/kaptinlin/jsonschema v0.4.6 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
@@ -55,15 +66,18 @@ require (
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 // indirect
github.com/livekit/mediatransportutil v0.0.0-20251128105421-19c7a7b81c22 // indirect
github.com/livekit/psrpc v0.7.1 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/magefile/mage v1.15.1-0.20250615140142-78acbaf2e3ae // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
github.com/pion/datachannel v1.6.0 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
@@ -81,11 +95,21 @@ require (
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/turn/v4 v4.1.4 // indirect
github.com/pion/webrtc/v4 v4.2.9 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchtv/twirp v8.1.3+incompatible // indirect
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
@@ -94,7 +118,7 @@ require (
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
@@ -104,8 +128,10 @@ require (
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
rsc.io/binaryregexp v0.2.0 // indirect
)
+63
View File
@@ -14,12 +14,16 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/Shopify/toxiproxy/v2 v2.12.0 h1:d1x++lYZg/zijXPPcv7PH0MvHMzEI5aX/YuUi/Sw+yg=
github.com/Shopify/toxiproxy/v2 v2.12.0/go.mod h1:R9Z38Pw6k2cGZWXHe7tbxjGW9azmY1KbDQJ1kd+h7Tk=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
@@ -38,6 +42,11 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
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/corazawaf/coraza/v3 v3.6.0 h1:rfsGl6eRBzzUAyADFcpuO7qXLt0DZtYWhfTIuhcyAjQ=
github.com/corazawaf/coraza/v3 v3.6.0/go.mod h1:q7gszZCSufoHIy9jV2NCgk+glYwZpP2mIKgbu2dZkvE=
github.com/corazawaf/libinjection-go v0.3.2 h1:9rrKt0lpg4WvUXt+lwS06GywfqRXXsa/7JcOw5cQLwI=
github.com/corazawaf/libinjection-go v0.3.2/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -77,6 +86,11 @@ 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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
@@ -92,12 +106,22 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 h1:b70jEaX2iaJSPZULSUxKtm73LBfsCrMsIlYCUgNGSIs=
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976/go.mod h1:ZGQeOwybjD8lkCjIyJfqR5LD2wMVHJ31d6GdPxoTsWY=
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 h1:c7gcNWTSr1gtLp6PyYi3wzvFCEcHJ4YRobDgqmIgf7Q=
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092/go.mod h1:ZZAN4fkkful3l1lpJwF8JbW41ZiG9TwJ2ZlqzQovBNU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/kaptinlin/go-i18n v0.1.4 h1:wCiwAn1LOcvymvWIVAM4m5dUAMiHunTdEubLDk4hTGs=
github.com/kaptinlin/go-i18n v0.1.4/go.mod h1:g1fn1GvTgT4CiLE8/fFE1hboHWJ6erivrDpiDtCcFKg=
github.com/kaptinlin/jsonschema v0.4.6 h1:vOSFg5tjmfkOdKg+D6Oo4fVOM/pActWu/ntkPsI1T64=
github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -135,6 +159,12 @@ github.com/livekit/server-sdk-go/v2 v2.16.0 h1:xbr6PLprgasruzEk4Qv2sHVcK6r+cebUv
github.com/livekit/server-sdk-go/v2 v2.16.0/go.mod h1:+HCKTpzV21b/jvBtu+OmWbquUxaL74kHLI9ZwKmdhKU=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.15.1-0.20250615140142-78acbaf2e3ae h1:yyMUG1VUd6IjV5jonMKpLXgwm9AzkfRsYisdCXc5OVI=
github.com/magefile/mage v1.15.1-0.20250615140142-78acbaf2e3ae/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
@@ -153,6 +183,8 @@ 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/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
@@ -169,6 +201,8 @@ github.com/opencontainers/runc v1.3.3 h1:qlmBbbhu+yY0QM7jqfuat7M1H3/iXjju3VkP9lk
github.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE=
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
@@ -211,6 +245,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
@@ -222,6 +264,10 @@ github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWg
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w=
github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
@@ -232,8 +278,17 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
@@ -287,6 +342,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -297,9 +354,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -336,6 +395,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=
@@ -369,3 +430,5 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+195
View File
@@ -0,0 +1,195 @@
// k6 WebSocket load test for OwnCord server
// Run: k6 run --vus 50 --duration 60s scripts/k6/ws-load.js
//
// Environment variables:
// K6_WS_URL - WebSocket URL (default: ws://localhost:8443/ws)
// K6_HTTP_URL - HTTP base URL (default: http://localhost:8443)
// K6_USERNAME - Test user prefix (default: loadtest)
// K6_PASSWORD - Test user password (default: LoadTest123!)
// K6_CHANNEL_ID - Channel ID to send messages in (default: 1)
import ws from "k6/ws";
import http from "k6/http";
import { check, sleep } from "k6";
import { Counter, Rate, Trend } from "k6/metrics";
// Custom metrics
const wsConnections = new Counter("ws_connections");
const wsMessages = new Counter("ws_messages_sent");
const wsErrors = new Counter("ws_errors");
const wsConnectTime = new Trend("ws_connect_time", true);
const wsMessageRate = new Rate("ws_message_success");
const authTime = new Trend("auth_time", true);
// Configuration
const WS_URL = __ENV.K6_WS_URL || "ws://localhost:8443/ws";
const HTTP_URL = __ENV.K6_HTTP_URL || "http://localhost:8443";
const USERNAME_PREFIX = __ENV.K6_USERNAME || "loadtest";
const PASSWORD = __ENV.K6_PASSWORD || "LoadTest123!";
const CHANNEL_ID = parseInt(__ENV.K6_CHANNEL_ID || "1");
export const options = {
scenarios: {
// Ramp up connections gradually
websocket_load: {
executor: "ramping-vus",
startVUs: 0,
stages: [
{ duration: "10s", target: 10 }, // warm up
{ duration: "30s", target: 50 }, // ramp to 50
{ duration: "60s", target: 50 }, // sustain
{ duration: "10s", target: 100 }, // spike
{ duration: "30s", target: 100 }, // sustain spike
{ duration: "10s", target: 0 }, // ramp down
],
},
},
thresholds: {
ws_connect_time: ["p(95)<2000"], // 95% connect under 2s
ws_message_success: ["rate>0.95"], // 95% message success
ws_errors: ["count<50"], // fewer than 50 errors
auth_time: ["p(95)<1000"], // 95% auth under 1s
},
};
// Login and get session token
function authenticate(username) {
const start = Date.now();
const res = http.post(
`${HTTP_URL}/api/v1/auth/login`,
JSON.stringify({ username, password: PASSWORD }),
{ headers: { "Content-Type": "application/json" } },
);
authTime.add(Date.now() - start);
if (res.status !== 200) {
wsErrors.add(1);
return null;
}
const body = JSON.parse(res.body);
return body.token;
}
export default function () {
const vuId = __VU;
const username = `${USERNAME_PREFIX}${vuId}`;
// Authenticate
const token = authenticate(username);
if (!token) {
sleep(1);
return;
}
// Connect WebSocket
const connectStart = Date.now();
const res = ws.connect(WS_URL, null, function (socket) {
wsConnectTime.add(Date.now() - connectStart);
wsConnections.add(1);
// Send auth on connect
socket.send(
JSON.stringify({
type: "auth",
token: token,
}),
);
// Handle incoming messages
socket.on("message", function (msg) {
try {
const data = JSON.parse(msg);
// After auth_ok, focus a channel and start sending
if (data.type === "ready") {
socket.send(
JSON.stringify({
type: "channel_focus",
channel_id: CHANNEL_ID,
}),
);
}
} catch (_e) {
wsErrors.add(1);
}
});
socket.on("error", function (_e) {
wsErrors.add(1);
});
// Send messages periodically (respecting rate limits)
let msgCount = 0;
const maxMessages = 10;
socket.setInterval(function () {
if (msgCount >= maxMessages) {
socket.close();
return;
}
const msg = JSON.stringify({
type: "chat_send",
channel_id: CHANNEL_ID,
content: `Load test message ${vuId}-${msgCount} at ${Date.now()}`,
});
socket.send(msg);
wsMessages.add(1);
wsMessageRate.add(true);
msgCount++;
}, 2000); // 1 message every 2 seconds (well under rate limit)
// Send typing indicators
socket.setInterval(function () {
if (msgCount < maxMessages) {
socket.send(
JSON.stringify({
type: "typing",
channel_id: CHANNEL_ID,
}),
);
}
}, 4000); // 1 typing every 4 seconds (under 1/3s limit)
// Send presence updates
socket.setInterval(function () {
socket.send(
JSON.stringify({
type: "presence",
status: "online",
}),
);
}, 15000); // 1 presence every 15 seconds (under 1/10s limit)
// Keep connection alive for the test duration
socket.setTimeout(function () {
socket.close();
}, 25000);
});
check(res, {
"WebSocket status is 101": (r) => r && r.status === 101,
});
if (!res || res.status !== 101) {
wsErrors.add(1);
wsMessageRate.add(false);
}
sleep(1);
}
export function handleSummary(data) {
return {
stdout: textSummary(data, { indent: " ", enableColors: true }),
"reports/k6-summary.json": JSON.stringify(data, null, 2),
};
}
// Built-in k6 text summary
function textSummary(data, opts) {
// k6 handles this automatically when not overridden
return JSON.stringify(data, null, 2);
}
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# Toxiproxy chaos testing for OwnCord
#
# Prerequisites:
# 1. toxiproxy-server running: toxiproxy-server &
# 2. OwnCord server running on port 8443
# 3. toxiproxy-cli available in PATH
#
# Usage: bash scripts/toxiproxy/chaos-test.sh
set -euo pipefail
TOXI_HOST="${TOXI_HOST:-localhost:8474}"
SERVER_HOST="${SERVER_HOST:-localhost}"
SERVER_PORT="${SERVER_PORT:-8443}"
PROXY_PORT="${PROXY_PORT:-18443}"
echo "=== OwnCord Chaos Testing ==="
echo "Toxiproxy API: $TOXI_HOST"
echo "Target server: $SERVER_HOST:$SERVER_PORT"
echo "Proxy port: $PROXY_PORT"
echo ""
# Create proxy
echo "[1/7] Creating proxy..."
toxiproxy-cli create owncord \
--listen "0.0.0.0:$PROXY_PORT" \
--upstream "$SERVER_HOST:$SERVER_PORT" 2>/dev/null || \
echo " (proxy already exists)"
echo ""
echo "[2/7] Test: Normal connectivity (baseline)"
echo " Connect to localhost:$PROXY_PORT and verify response..."
curl -sf "http://localhost:$PROXY_PORT/api/v1/health" && echo " OK" || echo " FAIL"
sleep 1
echo ""
echo "[3/7] Test: High latency (500ms)"
echo " Simulates slow network / cross-region..."
toxiproxy-cli toxic add owncord --type latency \
--attribute latency=500 --attribute jitter=100 \
--toxicName latency_test 2>/dev/null
echo " Running health check with latency..."
time curl -sf "http://localhost:$PROXY_PORT/api/v1/health" && echo " OK" || echo " FAIL"
toxiproxy-cli toxic remove owncord --toxicName latency_test
sleep 1
echo ""
echo "[4/7] Test: Packet loss (30%)"
echo " Simulates unreliable WiFi..."
toxiproxy-cli toxic add owncord --type timeout \
--attribute timeout=3000 \
--toxicName timeout_test 2>/dev/null
echo " Health check should timeout after 3s..."
timeout 5 curl -sf "http://localhost:$PROXY_PORT/api/v1/health" 2>/dev/null && echo " OK (fast)" || echo " Timed out as expected"
toxiproxy-cli toxic remove owncord --toxicName timeout_test
sleep 1
echo ""
echo "[5/7] Test: Bandwidth limit (10KB/s)"
echo " Simulates throttled connection..."
toxiproxy-cli toxic add owncord --type bandwidth \
--attribute rate=10 \
--toxicName bandwidth_test 2>/dev/null
echo " Health check with bandwidth limit..."
time curl -sf "http://localhost:$PROXY_PORT/api/v1/health" && echo " OK" || echo " FAIL"
toxiproxy-cli toxic remove owncord --toxicName bandwidth_test
sleep 1
echo ""
echo "[6/7] Test: Connection reset"
echo " Simulates abrupt disconnection..."
toxiproxy-cli toxic add owncord --type reset_peer \
--attribute timeout=1000 \
--toxicName reset_test 2>/dev/null
echo " Health check should fail after 1s..."
curl -sf --max-time 3 "http://localhost:$PROXY_PORT/api/v1/health" 2>/dev/null && echo " OK (unexpected)" || echo " Reset as expected"
toxiproxy-cli toxic remove owncord --toxicName reset_test
sleep 1
echo ""
echo "[7/7] Test: Downstream slicer (fragment responses)"
echo " Simulates packet fragmentation..."
toxiproxy-cli toxic add owncord --type slicer \
--attribute average_size=10 --attribute size_variation=5 --attribute delay=10 \
--toxicName slicer_test 2>/dev/null
echo " Health check with sliced responses..."
curl -sf "http://localhost:$PROXY_PORT/api/v1/health" && echo " OK" || echo " FAIL"
toxiproxy-cli toxic remove owncord --toxicName slicer_test
echo ""
echo "=== Cleanup ==="
toxiproxy-cli delete owncord 2>/dev/null || true
echo "Done. All chaos tests complete."