256 lines
5.7 KiB
Go
256 lines
5.7 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"crypto-miner-server/internal/models"
|
|
)
|
|
|
|
func openTestDB(t *testing.T) *Database {
|
|
t.Helper()
|
|
d, err := New(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { d.Close() })
|
|
return d
|
|
}
|
|
|
|
func seedAgent(t *testing.T, d *Database, id string) *models.Agent {
|
|
t.Helper()
|
|
a := &models.Agent{
|
|
ID: id,
|
|
Name: "worker-" + id,
|
|
Wallet: "wallet",
|
|
IP: "10.0.0.1",
|
|
Version: "2.0",
|
|
Status: "offline",
|
|
CPUCores: 4,
|
|
MemoryGB: 8,
|
|
LastSeen: time.Now(),
|
|
}
|
|
if err := d.UpsertAgent(a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func TestNewCreatesDatabase(t *testing.T) {
|
|
dir := t.TempDir()
|
|
d, err := New(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer d.Close()
|
|
|
|
var n int
|
|
if err := d.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='agents'").Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("expected agents table, got count %d", n)
|
|
}
|
|
}
|
|
|
|
func TestGetAgentNotFound(t *testing.T) {
|
|
d := openTestDB(t)
|
|
_, err := d.GetAgent("missing-agent")
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("expected sql.ErrNoRows, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpsertAgentPreservesCreatedAt(t *testing.T) {
|
|
d := openTestDB(t)
|
|
a := seedAgent(t, d, "persist-created")
|
|
|
|
first, err := d.GetAgent(a.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
a.Name = "renamed"
|
|
a.Status = "online"
|
|
if err := d.UpsertAgent(a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
second, err := d.GetAgent(a.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !second.CreatedAt.Equal(first.CreatedAt) {
|
|
t.Fatalf("created_at changed: %v -> %v", first.CreatedAt, second.CreatedAt)
|
|
}
|
|
if second.Name != "renamed" {
|
|
t.Fatalf("name not updated: %q", second.Name)
|
|
}
|
|
}
|
|
|
|
func TestUpdateAgentStats(t *testing.T) {
|
|
d := openTestDB(t)
|
|
a := seedAgent(t, d, "stats-agent")
|
|
|
|
if err := d.UpdateAgentStats(a.ID, 100, 200, 300, 10, 8, 2, 55.5, 66.6, 3600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := d.GetAgent(a.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Status != "online" {
|
|
t.Fatalf("status: got %q want online", got.Status)
|
|
}
|
|
if got.Hashrate15s != 100 || got.Hashrate1m != 200 || got.Hashrate15m != 300 {
|
|
t.Fatalf("hashrate mismatch: %+v", got)
|
|
}
|
|
if got.SharesTotal != 10 || got.SharesGood != 8 || got.SharesBad != 2 {
|
|
t.Fatalf("shares mismatch: %+v", got)
|
|
}
|
|
if got.CPUUsagePct != 55.5 || got.MemoryUsagePct != 66.6 || got.UptimeSeconds != 3600 {
|
|
t.Fatalf("usage/uptime mismatch: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSetAgentOffline(t *testing.T) {
|
|
d := openTestDB(t)
|
|
a := seedAgent(t, d, "offline-agent")
|
|
if err := d.UpdateAgentStats(a.ID, 1, 1, 1, 0, 0, 0, 0, 0, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := d.SetAgentOffline(a.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := d.GetAgent(a.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Status != "offline" {
|
|
t.Fatalf("status: got %q want offline", got.Status)
|
|
}
|
|
}
|
|
|
|
func TestShareInsertUpdateAndRecent(t *testing.T) {
|
|
d := openTestDB(t)
|
|
seedAgent(t, d, "share-agent")
|
|
|
|
ts := time.Now().UTC().Truncate(time.Second)
|
|
share := &models.Share{
|
|
AgentID: "share-agent",
|
|
JobID: "job-1",
|
|
Difficulty: 1000,
|
|
Accepted: false,
|
|
Hash: "abc123",
|
|
Nonce: "deadbeef",
|
|
Error: "pending",
|
|
Timestamp: ts,
|
|
}
|
|
id, err := d.InsertShare(share)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if id <= 0 {
|
|
t.Fatalf("expected positive insert id, got %d", id)
|
|
}
|
|
|
|
if err := d.UpdateShareResult(id, true, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
recent, err := d.GetRecentShares(10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(recent) != 1 {
|
|
t.Fatalf("expected 1 share, got %d", len(recent))
|
|
}
|
|
if !recent[0].Accepted {
|
|
t.Fatal("share should be accepted after update")
|
|
}
|
|
if recent[0].Error != "" {
|
|
t.Fatalf("error should be cleared, got %q", recent[0].Error)
|
|
}
|
|
|
|
if err := d.UpdateShareResult(id, false, "pool rejected"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recent, err = d.GetRecentShares(1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if recent[0].Accepted || recent[0].Error != "pool rejected" {
|
|
t.Fatalf("unexpected share after reject update: %+v", recent[0])
|
|
}
|
|
}
|
|
|
|
func TestHashrateSampleHistory(t *testing.T) {
|
|
d := openTestDB(t)
|
|
seedAgent(t, d, "hr-agent")
|
|
|
|
if err := d.InsertHashrateSample("hr-agent", 150.5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.InsertHashrateSample("hr-agent", 200.0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
history, err := d.GetHashrateHistory("hr-agent", 5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(history) != 2 {
|
|
t.Fatalf("expected 2 samples, got %d", len(history))
|
|
}
|
|
if history[0].Hashrate != 200.0 {
|
|
t.Fatalf("expected newest first, got %f", history[0].Hashrate)
|
|
}
|
|
|
|
empty, err := d.GetHashrateHistory("unknown", 5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(empty) != 0 {
|
|
t.Fatalf("expected empty history, got %d", len(empty))
|
|
}
|
|
}
|
|
|
|
func TestGetFleetStatsWithAgents(t *testing.T) {
|
|
d := openTestDB(t)
|
|
seedAgent(t, d, "fleet-a")
|
|
seedAgent(t, d, "fleet-b")
|
|
|
|
if err := d.UpdateAgentStats("fleet-a", 0, 0, 100, 20, 18, 2, 0, 0, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.UpdateAgentStats("fleet-b", 0, 0, 50, 10, 5, 5, 0, 0, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stats, err := d.GetFleetStats()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stats.TotalAgents != 2 {
|
|
t.Fatalf("total agents: got %d want 2", stats.TotalAgents)
|
|
}
|
|
if stats.OnlineAgents != 2 {
|
|
t.Fatalf("online agents: got %d want 2", stats.OnlineAgents)
|
|
}
|
|
if stats.TotalHashrate != 150 {
|
|
t.Fatalf("total hashrate: got %f want 150", stats.TotalHashrate)
|
|
}
|
|
if stats.TotalShares != 30 || stats.AcceptedShares != 23 || stats.RejectedShares != 7 {
|
|
t.Fatalf("share totals mismatch: %+v", stats)
|
|
}
|
|
wantRate := float64(23) / float64(30) * 100
|
|
if stats.AcceptRate != wantRate {
|
|
t.Fatalf("accept rate: got %f want %f", stats.AcceptRate, wantRate)
|
|
}
|
|
}
|