go-proxy/log/impl_test.go
Snider 2470f1ac3d feat(proxy): add log tests, fix nil config panic, complete test triads
- Add log package tests (AccessLog and ShareLog Good/Bad/Ugly triads)
- Fix nil pointer panic in pool.NewStrategyFactory when config is nil
- Add Worker Hashrate Good/Bad/Ugly test triad
- Add ConfigWatcher Start Bad test (nonexistent path)
- Add FailoverStrategy CurrentPools Bad/Ugly, EnabledPools Good/Bad/Ugly,
  and NewStrategyFactory Good/Bad/Ugly test triads
- Improve doc comments on Stats, StatsSummary, Workers, WorkerRecord
  with AX-compliant usage examples

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-05 08:08:28 +01:00

341 lines
9.7 KiB
Go

package log
import (
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"dappco.re/go/proxy"
)
// TestAccessLog_OnLogin_Good verifies a CONNECT line is written with the expected columns.
//
// al := log.NewAccessLog("/tmp/test-access.log")
// al.OnLogin(proxy.Event{Miner: miner}) // writes "CONNECT 10.0.0.1 WALLET XMRig/6.21.0"
func TestAccessLog_OnLogin_Good(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.log")
al := NewAccessLog(path)
defer al.Close()
miner := newTestMiner(t)
al.OnLogin(proxy.Event{Miner: miner})
al.Close()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected log file to exist: %v", err)
}
line := strings.TrimSpace(string(data))
if !strings.Contains(line, "CONNECT") {
t.Fatalf("expected CONNECT in log line, got %q", line)
}
}
// TestAccessLog_OnLogin_Bad verifies a nil miner event does not panic or write anything.
//
// al := log.NewAccessLog("/tmp/test-access.log")
// al.OnLogin(proxy.Event{Miner: nil}) // no-op
func TestAccessLog_OnLogin_Bad(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.log")
al := NewAccessLog(path)
defer al.Close()
al.OnLogin(proxy.Event{Miner: nil})
al.Close()
if _, err := os.Stat(path); err == nil {
data, _ := os.ReadFile(path)
if len(data) > 0 {
t.Fatalf("expected no output for nil miner, got %q", string(data))
}
}
}
// TestAccessLog_OnLogin_Ugly verifies a nil AccessLog does not panic.
//
// var al *log.AccessLog
// al.OnLogin(proxy.Event{Miner: miner}) // no-op, no panic
func TestAccessLog_OnLogin_Ugly(t *testing.T) {
var al *AccessLog
miner := newTestMiner(t)
al.OnLogin(proxy.Event{Miner: miner})
}
// TestAccessLog_OnClose_Good verifies a CLOSE line includes rx and tx byte counts.
//
// al := log.NewAccessLog("/tmp/test-access.log")
// al.OnClose(proxy.Event{Miner: miner}) // writes "CLOSE <ip> <user> rx=0 tx=0"
func TestAccessLog_OnClose_Good(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.log")
al := NewAccessLog(path)
defer al.Close()
miner := newTestMiner(t)
al.OnClose(proxy.Event{Miner: miner})
al.Close()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected log file to exist: %v", err)
}
line := strings.TrimSpace(string(data))
if !strings.Contains(line, "CLOSE") {
t.Fatalf("expected CLOSE in log line, got %q", line)
}
if !strings.Contains(line, "rx=") {
t.Fatalf("expected rx= in log line, got %q", line)
}
if !strings.Contains(line, "tx=") {
t.Fatalf("expected tx= in log line, got %q", line)
}
}
// TestAccessLog_OnClose_Bad verifies a nil miner close event produces no output.
//
// al := log.NewAccessLog("/tmp/test-access.log")
// al.OnClose(proxy.Event{Miner: nil}) // no-op
func TestAccessLog_OnClose_Bad(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.log")
al := NewAccessLog(path)
defer al.Close()
al.OnClose(proxy.Event{Miner: nil})
al.Close()
if _, err := os.Stat(path); err == nil {
data, _ := os.ReadFile(path)
if len(data) > 0 {
t.Fatalf("expected no output for nil miner, got %q", string(data))
}
}
}
// TestAccessLog_OnClose_Ugly verifies close on an empty-path log is a no-op.
//
// al := log.NewAccessLog("")
// al.OnClose(proxy.Event{Miner: miner}) // no-op, empty path
func TestAccessLog_OnClose_Ugly(t *testing.T) {
al := NewAccessLog("")
defer al.Close()
miner := newTestMiner(t)
al.OnClose(proxy.Event{Miner: miner})
}
// TestShareLog_OnAccept_Good verifies an ACCEPT line is written with diff and latency.
//
// sl := log.NewShareLog("/tmp/test-shares.log")
// sl.OnAccept(proxy.Event{Miner: miner, Diff: 100000, Latency: 82})
func TestShareLog_OnAccept_Good(t *testing.T) {
path := filepath.Join(t.TempDir(), "shares.log")
sl := NewShareLog(path)
defer sl.Close()
miner := newTestMiner(t)
sl.OnAccept(proxy.Event{Miner: miner, Diff: 100000, Latency: 82})
sl.Close()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected log file to exist: %v", err)
}
line := strings.TrimSpace(string(data))
if !strings.Contains(line, "ACCEPT") {
t.Fatalf("expected ACCEPT in log line, got %q", line)
}
if !strings.Contains(line, "diff=100000") {
t.Fatalf("expected diff=100000 in log line, got %q", line)
}
if !strings.Contains(line, "latency=82ms") {
t.Fatalf("expected latency=82ms in log line, got %q", line)
}
}
// TestShareLog_OnAccept_Bad verifies a nil miner accept event produces no output.
//
// sl := log.NewShareLog("/tmp/test-shares.log")
// sl.OnAccept(proxy.Event{Miner: nil}) // no-op
func TestShareLog_OnAccept_Bad(t *testing.T) {
path := filepath.Join(t.TempDir(), "shares.log")
sl := NewShareLog(path)
defer sl.Close()
sl.OnAccept(proxy.Event{Miner: nil, Diff: 100000})
sl.Close()
if _, err := os.Stat(path); err == nil {
data, _ := os.ReadFile(path)
if len(data) > 0 {
t.Fatalf("expected no output for nil miner, got %q", string(data))
}
}
}
// TestShareLog_OnAccept_Ugly verifies a nil ShareLog does not panic.
//
// var sl *log.ShareLog
// sl.OnAccept(proxy.Event{Miner: miner}) // no-op, no panic
func TestShareLog_OnAccept_Ugly(t *testing.T) {
var sl *ShareLog
miner := newTestMiner(t)
sl.OnAccept(proxy.Event{Miner: miner, Diff: 100000})
}
// TestShareLog_OnReject_Good verifies a REJECT line is written with the rejection reason.
//
// sl := log.NewShareLog("/tmp/test-shares.log")
// sl.OnReject(proxy.Event{Miner: miner, Error: "Low difficulty share"})
func TestShareLog_OnReject_Good(t *testing.T) {
path := filepath.Join(t.TempDir(), "shares.log")
sl := NewShareLog(path)
defer sl.Close()
miner := newTestMiner(t)
sl.OnReject(proxy.Event{Miner: miner, Error: "Low difficulty share"})
sl.Close()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected log file to exist: %v", err)
}
line := strings.TrimSpace(string(data))
if !strings.Contains(line, "REJECT") {
t.Fatalf("expected REJECT in log line, got %q", line)
}
if !strings.Contains(line, "Low difficulty share") {
t.Fatalf("expected rejection reason in log line, got %q", line)
}
}
// TestShareLog_OnReject_Bad verifies a nil miner reject event produces no output.
//
// sl := log.NewShareLog("/tmp/test-shares.log")
// sl.OnReject(proxy.Event{Miner: nil}) // no-op
func TestShareLog_OnReject_Bad(t *testing.T) {
path := filepath.Join(t.TempDir(), "shares.log")
sl := NewShareLog(path)
defer sl.Close()
sl.OnReject(proxy.Event{Miner: nil, Error: "Low difficulty share"})
sl.Close()
if _, err := os.Stat(path); err == nil {
data, _ := os.ReadFile(path)
if len(data) > 0 {
t.Fatalf("expected no output for nil miner, got %q", string(data))
}
}
}
// TestShareLog_OnReject_Ugly verifies an empty-path ShareLog silently discards the reject line.
//
// sl := log.NewShareLog("")
// sl.OnReject(proxy.Event{Miner: miner, Error: "reason"}) // no-op, empty path
func TestShareLog_OnReject_Ugly(t *testing.T) {
sl := NewShareLog("")
defer sl.Close()
miner := newTestMiner(t)
sl.OnReject(proxy.Event{Miner: miner, Error: "reason"})
}
// TestAccessLog_Close_Good verifies Close releases the file handle and is safe to call twice.
//
// al := log.NewAccessLog("/tmp/test-access.log")
// al.OnLogin(proxy.Event{Miner: miner})
// al.Close()
// al.Close() // double close is safe
func TestAccessLog_Close_Good(t *testing.T) {
path := filepath.Join(t.TempDir(), "access.log")
al := NewAccessLog(path)
miner := newTestMiner(t)
al.OnLogin(proxy.Event{Miner: miner})
al.Close()
al.Close()
}
// TestAccessLog_Close_Bad verifies Close on a nil AccessLog does not panic.
//
// var al *log.AccessLog
// al.Close() // no-op, no panic
func TestAccessLog_Close_Bad(t *testing.T) {
var al *AccessLog
al.Close()
}
// TestAccessLog_Close_Ugly verifies Close on a never-opened log does not panic.
//
// al := log.NewAccessLog("/nonexistent/dir/access.log")
// al.Close() // no file was ever opened
func TestAccessLog_Close_Ugly(t *testing.T) {
al := NewAccessLog("/nonexistent/dir/access.log")
al.Close()
}
// TestShareLog_Close_Good verifies Close releases the file handle and is safe to call twice.
//
// sl := log.NewShareLog("/tmp/test-shares.log")
// sl.OnAccept(proxy.Event{Miner: miner, Diff: 1000})
// sl.Close()
// sl.Close() // double close is safe
func TestShareLog_Close_Good(t *testing.T) {
path := filepath.Join(t.TempDir(), "shares.log")
sl := NewShareLog(path)
miner := newTestMiner(t)
sl.OnAccept(proxy.Event{Miner: miner, Diff: 1000})
sl.Close()
sl.Close()
}
// TestShareLog_Close_Bad verifies Close on a nil ShareLog does not panic.
//
// var sl *log.ShareLog
// sl.Close() // no-op, no panic
func TestShareLog_Close_Bad(t *testing.T) {
var sl *ShareLog
sl.Close()
}
// TestShareLog_Close_Ugly verifies Close on a never-opened log does not panic.
//
// sl := log.NewShareLog("/nonexistent/dir/shares.log")
// sl.Close() // no file was ever opened
func TestShareLog_Close_Ugly(t *testing.T) {
sl := NewShareLog("/nonexistent/dir/shares.log")
sl.Close()
}
// newTestMiner creates a minimal miner for log testing using a net.Pipe connection.
func newTestMiner(t *testing.T) *proxy.Miner {
t.Helper()
client, server := net.Pipe()
t.Cleanup(func() {
_ = client.Close()
_ = server.Close()
})
miner := proxy.NewMiner(client, 3333, nil)
miner.SetID(1)
return miner
}
// pipeAddr satisfies the net.Addr interface for pipe-based test connections.
type pipeAddr struct{}
func (a pipeAddr) Network() string { return "pipe" }
func (a pipeAddr) String() string { return "pipe" }
// pipeConn wraps an os.Pipe as a net.Conn for tests that need a closeable socket.
type pipeConn struct {
*os.File
}
func (p *pipeConn) RemoteAddr() net.Addr { return pipeAddr{} }
func (p *pipeConn) LocalAddr() net.Addr { return pipeAddr{} }
func (p *pipeConn) SetDeadline(_ time.Time) error { return nil }
func (p *pipeConn) SetReadDeadline(_ time.Time) error { return nil }
func (p *pipeConn) SetWriteDeadline(_ time.Time) error { return nil }