ws and pruner done!!!

This commit is contained in:
41666 2024-10-28 13:46:52 -07:00
parent c5cc245e25
commit 74add408e6
34 changed files with 1455 additions and 221 deletions

2
.envrc
View file

@ -1,2 +1,2 @@
source .env; dotenv;
use flake; use flake;

52
cmd/pruner/pruner.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"context"
"log"
"os"
"sync"
"time"
"github.com/genudine/saerro-go/store"
"github.com/genudine/saerro-go/util"
)
func main() {
db, err := util.GetDBConnection(os.Getenv("DB_ADDR"))
if err != nil {
log.Fatalln(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
playerStore := store.NewPlayerStore(db)
i, err := playerStore.Prune(ctx)
if err != nil {
log.Println("pruner: playerStore.Prune failed")
}
log.Printf("pruner: deleted %d players", i)
}()
wg.Add(1)
go func() {
defer wg.Done()
vehicleStore := store.NewVehicleStore(db)
i, err := vehicleStore.Prune(ctx)
if err != nil {
log.Println("pruner: vehicleStore.Prune failed")
}
log.Printf("pruner: deleted %d vehicles", i)
}()
wg.Wait()
}

View file

@ -1,72 +0,0 @@
package main
import (
"context"
"database/sql"
"testing"
"time"
"github.com/genudine/saerro-go/translators"
"github.com/genudine/saerro-go/types"
"github.com/stretchr/testify/assert"
_ "modernc.org/sqlite"
)
func getEventHandlerTestShim(t *testing.T) (EventHandler, *sql.DB) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("test shim: sqlite open failed, %v", err)
}
return EventHandler{
Ingest: &Ingest{
DB: db,
},
}, db
}
func TestHandleDeath(t *testing.T) {
eh, db := getEventHandlerTestShim(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
event := types.ESSEvent{
EventName: "Death",
WorldID: 17,
ZoneID: 2,
CharacterID: "DollNC",
LoadoutID: "3",
TeamID: types.NC,
AttackerCharacterID: "Lyyti",
AttackerLoadoutID: "3",
AttackerTeamID: types.TR,
}
eh.HandleDeath(ctx, event)
type player struct {
CharacterID string `json:"character_id"`
ClassName string `json:"class_name"`
}
var player1 player
err := db.QueryRowContext(ctx, "SELECT * FROM players WHERE character_id = ?", event.CharacterID).Scan(&player1)
if err != nil {
t.Error(err)
}
assert.Equal(t, event.CharacterID, player1.CharacterID)
assert.Equal(t, translators.ClassFromLoadout(event.LoadoutID), player1.ClassName)
var player2 player
err = db.QueryRowContext(ctx, "SELECT * FROM players WHERE character_id = ?", event.AttackerCharacterID).Scan(&player2)
if err != nil {
t.Error(err)
}
assert.Equal(t, event.AttackerCharacterID, player2.CharacterID)
assert.Equal(t, translators.ClassFromLoadout(event.AttackerLoadoutID), player2.ClassName)
}

View file

@ -1,19 +1,30 @@
package main package eventhandler
import ( import (
"context" "context"
"log" "database/sql"
"github.com/genudine/saerro-go/cmd/ws/ingest"
"github.com/genudine/saerro-go/store"
"github.com/genudine/saerro-go/types" "github.com/genudine/saerro-go/types"
) )
type EventHandler struct { type EventHandler struct {
Ingest *Ingest Ingest *ingest.Ingest
}
func NewEventHandler(db *sql.DB) EventHandler {
return EventHandler{
Ingest: &ingest.Ingest{
PlayerStore: store.NewPlayerStore(db),
},
}
} }
func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) { func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) {
if event.EventName == "" { if event.EventName == "" {
log.Println("invalid event; dropping") // log.Println("invalid event; dropping")
return
} }
if event.EventName == "Death" || event.EventName == "VehicleDestroy" { if event.EventName == "Death" || event.EventName == "VehicleDestroy" {
@ -27,14 +38,14 @@ func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) {
func (eh *EventHandler) HandleDeath(ctx context.Context, event types.ESSEvent) { func (eh *EventHandler) HandleDeath(ctx context.Context, event types.ESSEvent) {
if event.CharacterID != "" && event.CharacterID != "0" { if event.CharacterID != "" && event.CharacterID != "0" {
log.Println("got pop event") // log.Println("got pop event")
pe := PopEventFromESSEvent(event, false) pe := types.PopEventFromESSEvent(event, false)
eh.Ingest.TrackPop(ctx, pe) eh.Ingest.TrackPop(ctx, pe)
} }
if event.AttackerCharacterID != "" && event.AttackerCharacterID != "0" && event.AttackerTeamID != 0 { if event.AttackerCharacterID != "" && event.AttackerCharacterID != "0" && event.AttackerTeamID != 0 {
log.Println("got attacker pop event") pe := types.PopEventFromESSEvent(event, true)
pe := PopEventFromESSEvent(event, true) // fmt.Println("got attacker pop event", event)
eh.Ingest.TrackPop(ctx, pe) eh.Ingest.TrackPop(ctx, pe)
} }
} }
@ -45,18 +56,16 @@ func (eh *EventHandler) HandleExperience(ctx context.Context, event types.ESSEve
switch event.ExperienceID { switch event.ExperienceID {
case 201: // Galaxy Spawn Bonus case 201: // Galaxy Spawn Bonus
vehicleID = "11" vehicleID = "11"
break
case 233: // Sunderer Spawn Bonus case 233: // Sunderer Spawn Bonus
vehicleID = "2" vehicleID = "2"
break case 674:
case 674: // ANT stuff fallthrough // ANT stuff
case 675: case 675:
vehicleID = "160" vehicleID = "160"
break
} }
event.VehicleID = vehicleID event.VehicleID = vehicleID
pe := PopEventFromESSEvent(event, false) pe := types.PopEventFromESSEvent(event, false)
eh.Ingest.TrackPop(ctx, pe) eh.Ingest.TrackPop(ctx, pe)
} }

View file

@ -0,0 +1,144 @@
package eventhandler
import (
"context"
"testing"
"time"
"github.com/avast/retry-go"
"github.com/stretchr/testify/assert"
"github.com/genudine/saerro-go/cmd/ws/ingest"
"github.com/genudine/saerro-go/store"
"github.com/genudine/saerro-go/translators"
"github.com/genudine/saerro-go/types"
"github.com/genudine/saerro-go/util/testutil"
)
func getEventHandlerTestShim(t *testing.T) (EventHandler, context.Context) {
t.Helper()
db := testutil.GetTestDB(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
t.Cleanup(cancel)
return EventHandler{
Ingest: &ingest.Ingest{
PlayerStore: store.NewPlayerStore(db),
},
}, ctx
}
func TestHandleDeath(t *testing.T) {
eh, ctx := getEventHandlerTestShim(t)
event := types.ESSEvent{
EventName: "Death",
WorldID: 17,
ZoneID: 2,
CharacterID: "LyytisDoll",
LoadoutID: 3,
TeamID: types.NC,
AttackerCharacterID: "Lyyti",
AttackerLoadoutID: 3,
AttackerTeamID: types.TR,
}
eh.HandleDeath(ctx, event)
player1, err := eh.Ingest.PlayerStore.GetOne(ctx, event.CharacterID)
assert.NoError(t, err, "player1 fetch failed")
assert.Equal(t, event.CharacterID, player1.CharacterID)
assert.Equal(t, string(translators.ClassFromLoadout(event.LoadoutID)), player1.ClassName)
player2, err := eh.Ingest.PlayerStore.GetOne(ctx, event.AttackerCharacterID)
assert.NoError(t, err, "player2 fetch failed")
assert.Equal(t, event.AttackerCharacterID, player2.CharacterID)
assert.Equal(t, string(translators.ClassFromLoadout(event.AttackerLoadoutID)), player2.ClassName)
}
func TestHandleExperience(t *testing.T) {
eh, ctx := getEventHandlerTestShim(t)
event := types.ESSEvent{
EventName: "GainExperience",
WorldID: 17,
ZoneID: 2,
CharacterID: "LyytisDoll",
LoadoutID: 3,
TeamID: types.NC,
ExperienceID: 674,
}
eh.HandleExperience(ctx, event)
player, err := eh.Ingest.PlayerStore.GetOne(ctx, event.CharacterID)
assert.NoError(t, err, "player fetch check failed")
assert.Equal(t, event.CharacterID, player.CharacterID)
assert.Equal(t, string(translators.ClassFromLoadout(event.LoadoutID)), player.ClassName)
}
func TestHandleAnalytics(t *testing.T) {
eh, ctx := getEventHandlerTestShim(t)
event := types.ESSEvent{
EventName: "GainExperience",
WorldID: 17,
ZoneID: 2,
CharacterID: "LyytisDoll",
LoadoutID: 3,
TeamID: types.NC,
ExperienceID: 674,
}
eh.HandleAnalytics(ctx, event)
}
func TestHandleEvent(t *testing.T) {
eh, ctx := getEventHandlerTestShim(t)
events := []types.ESSEvent{
{
EventName: "Death",
WorldID: 17,
ZoneID: 2,
CharacterID: "LyytisDoll",
LoadoutID: 3,
TeamID: types.NC,
AttackerCharacterID: "Lyyti",
AttackerLoadoutID: 3,
AttackerTeamID: types.TR,
},
{
EventName: "GainExperience",
WorldID: 17,
ZoneID: 2,
CharacterID: "DollNC",
LoadoutID: 3,
TeamID: types.NC,
ExperienceID: 201,
},
}
for _, event := range events {
eh.HandleEvent(ctx, event)
}
checkPlayers := []string{"LyytisDoll", "Lyyti", "DollNC"}
for _, id := range checkPlayers {
// eventual consistency <333
err := retry.Do(func() error {
_, err := eh.Ingest.PlayerStore.GetOne(ctx, id)
return err
})
assert.NoError(t, err)
}
}

View file

@ -1,14 +0,0 @@
package main
import (
"context"
"database/sql"
)
type Ingest struct {
DB *sql.DB
}
func (i *Ingest) TrackPop(ctx context.Context, event PopEvent) {
}

55
cmd/ws/ingest/ingest.go Normal file
View file

@ -0,0 +1,55 @@
package ingest
import (
"context"
"fmt"
"log"
"github.com/genudine/saerro-go/store"
"github.com/genudine/saerro-go/types"
)
type Ingest struct {
PlayerStore store.IPlayerStore
}
func (i *Ingest) TrackPop(ctx context.Context, event types.PopEvent) {
player := event.ToPlayer()
err := i.fixupPlayer(ctx, player)
if err != nil {
log.Println("ingest: player fixup failed, dropping event", err)
return
}
err = i.PlayerStore.Insert(ctx, player)
if err != nil {
log.Println("TrackPop Insert failed", err)
}
}
func (i *Ingest) fixupPlayer(ctx context.Context, player *types.Player) error {
if player.ClassName != "unknown" && player.FactionID != 0 {
// all fixups are done
return nil
}
storedPlayer, err := i.PlayerStore.GetOne(ctx, player.CharacterID)
if err != nil {
return fmt.Errorf("ingest: fixupPlayer: fetching player %s failed: %w", player.CharacterID, err)
}
// probably VehicleDestroy
if player.ClassName == "unknown" {
// TODO: maybe get this from census, profile_id
player.ClassName = storedPlayer.ClassName
}
// probably PS4
if player.FactionID == 0 {
// TODO: get this from census
player.FactionID = storedPlayer.FactionID
}
return nil
}

View file

@ -0,0 +1,84 @@
package ingest_test
import (
"context"
"errors"
"testing"
"time"
"github.com/genudine/saerro-go/cmd/ws/ingest"
"github.com/genudine/saerro-go/store/storemock"
"github.com/genudine/saerro-go/translators"
"github.com/genudine/saerro-go/types"
)
func mkIngest(t *testing.T) (context.Context, *ingest.Ingest, *storemock.MockPlayerStore) {
t.Helper()
ps := new(storemock.MockPlayerStore)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
t.Cleanup(cancel)
i := &ingest.Ingest{
PlayerStore: ps,
}
return ctx, i, ps
}
func TestTrackPopHappyPath(t *testing.T) {
ctx, i, ps := mkIngest(t)
// Combat Medic on Emerald
event := types.PopEvent{
WorldID: 17,
ZoneID: 4,
TeamID: types.TR,
LoadoutID: 4,
ClassName: translators.CombatMedic,
CharacterID: "aaaa",
}
eventPlayer := event.ToPlayer()
ps.On("Insert", ctx, eventPlayer).Return(nil).Once()
i.TrackPop(ctx, event)
}
func TestTrackPopFixup(t *testing.T) {
ctx, i, ps := mkIngest(t)
event := types.PopEvent{
WorldID: 17,
ZoneID: 4,
TeamID: 0,
ClassName: "unknown",
CharacterID: "bbbb",
}
pastEventPlayer := event.ToPlayer()
pastEventPlayer.ClassName = "light_assault"
pastEventPlayer.FactionID = types.VS
ps.On("GetOne", ctx, event.CharacterID).Return(pastEventPlayer, nil).Once()
ps.On("Insert", ctx, pastEventPlayer).Return(nil).Once()
i.TrackPop(ctx, event)
}
func TestTrackPopFixupFailed(t *testing.T) {
ctx, i, ps := mkIngest(t)
event := types.PopEvent{
WorldID: 17,
ZoneID: 4,
TeamID: 0,
ClassName: "unknown",
CharacterID: "bbbb",
}
ps.On("GetOne", ctx, event.CharacterID).Return(nil, errors.New("ingest fixup failed")).Once()
i.TrackPop(ctx, event)
}

View file

@ -2,14 +2,15 @@ package main
import ( import (
"context" "context"
"database/sql"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"time" "time"
"github.com/genudine/saerro-go/types" "github.com/genudine/saerro-go/cmd/ws/eventhandler"
"nhooyr.io/websocket" "github.com/genudine/saerro-go/cmd/ws/wsmanager"
"nhooyr.io/websocket/wsjson" "github.com/genudine/saerro-go/util"
) )
func main() { func main() {
@ -18,54 +19,41 @@ func main() {
log.Fatalln("WS_ADDR is not set.") log.Fatalln("WS_ADDR is not set.")
} }
db, err := sql.Open("sqlite", ":memory:") db, err := util.GetDBConnection(os.Getenv("DB_ADDR"))
if err != nil { if err != nil {
log.Fatalln("database connection failed", err) log.Fatalln(err)
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) eventHandler := eventhandler.NewEventHandler(db)
wsm := wsmanager.NewWebsocketManager(eventHandler)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel() defer cancel()
wsConn, _, err := websocket.Dial(ctx, wsAddr, nil) err = wsm.Connect(ctx, wsAddr)
if err != nil { if err != nil {
log.Fatalln("Connection to ESS failed.", err) log.Fatalln(err)
}
defer wsConn.Close(websocket.StatusInternalError, "internal error. bye")
err = wsjson.Write(ctx, wsConn, map[string]interface{}{
"action": "subscribe",
"worlds": "all",
"eventNames": getEventNames(),
"characters": []string{"all"},
"service": "event",
"logicalAndCharactersWithWorlds": true,
})
if err != nil {
log.Fatalln("subscription write failed", err)
} }
log.Println("subscribe done") go wsm.Start()
eventHandler := EventHandler{ go func() {
Ingest: &Ingest{ time.Sleep(time.Second * 1)
DB: db, err = wsm.Subscribe(ctx)
},
}
for {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
var event types.ESSData
err := wsjson.Read(ctx, wsConn, &event)
if err != nil { if err != nil {
log.Println("wsjson read failed", err) wsm.FailClose()
cancel() log.Fatalln("subscribe failed", err)
continue
} }
log.Println("sent subscribe")
}()
go eventHandler.HandleEvent(ctx, event.Payload) exitSignal := make(chan os.Signal, 1)
signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM)
select {
case <-exitSignal:
log.Println("got interrupt, exiting...")
case <-wsm.Closed:
log.Println("websocket closed, bailing...")
} }
wsConn.Close(websocket.StatusNormalClosure, "")
} }

View file

@ -1,4 +1,4 @@
package main package wsmanager
import ( import (
"fmt" "fmt"

View file

@ -1,4 +1,4 @@
package main package wsmanager
import ( import (
"testing" "testing"

View file

@ -0,0 +1,124 @@
package wsmanager
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/coder/websocket"
"github.com/genudine/saerro-go/cmd/ws/eventhandler"
"github.com/genudine/saerro-go/types"
)
type WebsocketManager struct {
Conn *websocket.Conn
EventHandler eventhandler.EventHandler
Closed chan bool
}
func NewWebsocketManager(eh eventhandler.EventHandler) WebsocketManager {
return WebsocketManager{
EventHandler: eh,
Closed: make(chan bool, 1),
}
}
func (wsm *WebsocketManager) Connect(ctx context.Context, addr string) (err error) {
wsm.Conn, _, err = websocket.Dial(ctx, addr, nil)
if err != nil {
return fmt.Errorf("wsm: connect failed: %w", err)
}
log.Println("wsm: connected to", addr)
return
}
type ESSSubscription struct {
Action string `json:"action,omitempty"`
Worlds []string `json:"worlds,omitempty"`
EventNames []string `json:"eventNames,omitempty"`
Characters []string `json:"characters,omitempty"`
Service string `json:"service,omitempty"`
LogicalAndCharactersWithWorlds bool `json:"logicalAndCharactersWithWorlds,omitempty"`
}
func (wsm *WebsocketManager) Subscribe(ctx context.Context) error {
sub := ESSSubscription{
Action: "subscribe",
Service: "event",
Worlds: []string{"all"},
EventNames: getEventNames(),
Characters: []string{"all"},
LogicalAndCharactersWithWorlds: true,
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(sub)
if err != nil {
return fmt.Errorf("wsm: subscribe: json encode failed: %w", err)
}
log.Printf("wsm: subscribe message: %s", buf.String())
err = wsm.Conn.Write(ctx, websocket.MessageText, buf.Bytes())
if err != nil {
return fmt.Errorf("wsm: subscribe: ws write failed: %w", err)
}
return nil
}
func (wsm *WebsocketManager) Start() {
go wsm.startWatchdog()
for {
ctx := context.Background()
var event types.ESSData
_, data, err := wsm.Conn.Read(ctx)
if err != nil {
log.Fatalln("wsm: read failed:", err)
}
// log.Printf("raw event: %s", string(data))
err = json.Unmarshal(data, &event)
if err != nil {
log.Println("wsm: json unmarshal failed:", err)
log.Println("wsm: json unmarshal failed (payload)", string(data))
}
go wsm.EventHandler.HandleEvent(ctx, event.Payload)
}
}
func (wsm *WebsocketManager) startWatchdog() {
for {
time.Sleep(time.Second * 30)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
err := wsm.Conn.Ping(ctx)
if err != nil {
log.Println("wsm: watchdog failed")
wsm.Closed <- true
}
cancel()
}
}
func (wsm *WebsocketManager) Close() {
wsm.Conn.Close(websocket.StatusNormalClosure, "")
wsm.Closed <- true
}
func (wsm *WebsocketManager) FailClose() {
wsm.Conn.Close(websocket.StatusAbnormalClosure, "")
wsm.Closed <- true
}

View file

@ -1,4 +1,3 @@
services: services:
tsdb: tsdb:
image: docker.io/timescale/timescaledb:latest-pg14 image: docker.io/timescale/timescaledb:latest-pg14
@ -6,5 +5,6 @@ services:
POSTGRES_PASSWORD: saerro321 POSTGRES_PASSWORD: saerro321
POSTGRES_USER: saerrouser POSTGRES_USER: saerrouser
POSTGRES_DB: data POSTGRES_DB: data
network_mode: host
ports: ports:
- 5432:5432 - 5432:5432

25
go.mod
View file

@ -3,23 +3,38 @@ module github.com/genudine/saerro-go
go 1.22.3 go 1.22.3
require ( require (
github.com/glebarez/go-sqlite v1.22.0 github.com/avast/retry-go v3.0.0+incompatible
github.com/coder/websocket v1.8.12
github.com/jackc/pgx/v5 v5.7.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
nhooyr.io/websocket v1.8.11 modernc.org/sqlite v1.33.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.19.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.52.1 // indirect modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.30.1 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
) )

77
go.sum
View file

@ -1,13 +1,31 @@
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@ -16,29 +34,48 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo= modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM= modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@ -47,11 +84,9 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk= modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU= modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View file

@ -1,4 +1,28 @@
{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell { { pkgs ? import <nixpkgs> {} }: let
podmanSetupScript = let
registriesConf = pkgs.writeText "registries.conf" ''
[registries.search]
registries = ['docker.io']
[registries.block]
registries = []
'';
in pkgs.writeScript "podman-setup" ''
#!${pkgs.runtimeShell}
# Dont overwrite customised configuration
if ! test -f ~/.config/containers/policy.json; then
install -Dm555 ${pkgs.skopeo.src}/default-policy.json ~/.config/containers/policy.json
fi
if ! test -f ~/.config/containers/registries.conf; then
install -Dm555 ${registriesConf} ~/.config/containers/registries.conf
fi
'';
# Provides a fake "docker" binary mapping to podman
dockerCompat = pkgs.runCommandNoCC "docker-podman-compat" {} ''
mkdir -p $out/bin
ln -s ${pkgs.podman}/bin/podman $out/bin/docker
'';
in pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
go go
just just

23
store/env_test.go Normal file
View file

@ -0,0 +1,23 @@
package store_test
import (
"context"
"database/sql"
"testing"
"time"
"github.com/genudine/saerro-go/util/testutil"
)
func mkEnv(t *testing.T) (context.Context, *sql.DB) {
t.Helper()
db := testutil.GetTestDB(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
t.Cleanup(func() {
cancel()
})
return ctx, db
}

View file

@ -5,10 +5,26 @@ import (
"database/sql" "database/sql"
"log" "log"
"time" "time"
"github.com/genudine/saerro-go/types"
"github.com/genudine/saerro-go/util"
"github.com/avast/retry-go"
) )
type IPlayerStore interface {
IsMigrated(context.Context) bool
RunMigration(context.Context, bool)
Insert(context.Context, *types.Player) error
GetOne(context.Context, string) (*types.Player, error)
Prune(context.Context) (int64, error)
}
type PlayerStore struct { type PlayerStore struct {
DB *sql.DB DB *sql.DB
// Test introspection for if migrations ran during this PlayerStore init
RanMigration bool
} }
func NewPlayerStore(db *sql.DB) *PlayerStore { func NewPlayerStore(db *sql.DB) *PlayerStore {
@ -24,10 +40,19 @@ func NewPlayerStore(db *sql.DB) *PlayerStore {
return ps return ps
} }
func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) { func (ps *PlayerStore) IsMigrated(ctx context.Context) bool {
if !force { _, err := ps.DB.QueryContext(ctx, `SELECT count(1) FROM players LIMIT 1;`)
// check if migrated first... if err != nil {
log.Printf("IsMigrated check failed: %v", err)
return false
}
return true
}
func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
if !force && ps.IsMigrated(ctx) {
return
} }
log.Println("(Re)-creating players table") log.Println("(Re)-creating players table")
@ -36,13 +61,101 @@ func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
CREATE TABLE players ( CREATE TABLE players (
character_id TEXT NOT NULL PRIMARY KEY, character_id TEXT NOT NULL PRIMARY KEY,
last_updated TIMESTAMPTZ NOT NULL, last_updated TIMESTAMP NOT NULL,
world_id INT NOT NULL, world_id INT NOT NULL,
faction_id INT NOT NULL, faction_id INT NOT NULL,
zone_id INT NOT NULL, zone_id INT NOT NULL,
class_name TEXT NOT NULL class_name TEXT NOT NULL
); );
`)
-- TODO: Add indexes?
`)
log.Println("Done, players table is initialized.") log.Println("Done, players table is initialized.")
ps.RanMigration = true
}
// Insert a player into the store.
// For testing, when LastUpdated is not "zero", the provided timestamp will be carried into the store.
func (ps *PlayerStore) Insert(ctx context.Context, player *types.Player) error {
if player.LastUpdated.IsZero() {
player.LastUpdated = time.Now()
}
err := retry.Do(func() error {
_, err := ps.DB.ExecContext(ctx,
`
INSERT INTO players (
last_updated, character_id, world_id, faction_id, zone_id, class_name
)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (character_id) DO UPDATE SET
last_updated = EXCLUDED.last_updated,
world_id = EXCLUDED.world_id,
faction_id = EXCLUDED.faction_id,
zone_id = EXCLUDED.zone_id,
class_name = EXCLUDED.class_name;
`,
util.TimeToString(player.LastUpdated),
player.CharacterID,
player.WorldID,
player.FactionID,
player.ZoneID,
player.ClassName,
)
return err
}, retry.Attempts(2))
return err
}
// GetOne player from the store.
func (ps *PlayerStore) GetOne(ctx context.Context, id string) (*types.Player, error) {
row := ps.DB.QueryRowContext(ctx, `
SELECT
last_updated,
character_id,
world_id,
faction_id,
zone_id,
class_name
FROM players WHERE character_id = $1
`, id)
player := &types.Player{}
var timestamp string
err := row.Scan(
&timestamp,
&player.CharacterID,
&player.WorldID,
&player.FactionID,
&player.ZoneID,
&player.ClassName,
)
if err != nil {
return nil, err
}
player.LastUpdated, err = time.Parse(time.RFC3339, timestamp)
return player, err
}
func (ps *PlayerStore) Prune(ctx context.Context) (int64, error) {
log.Println("pruning PlayerStore")
// Avoid using sql idioms here for portability
// SQLite and PgSQL do now() differently, we don't need to at all.
res, err := ps.DB.ExecContext(ctx,
`
DELETE FROM players WHERE last_updated < $1;
`,
util.TimeToString(time.Now().Add(-time.Minute*15)),
)
if err != nil {
return 0, err
}
return res.RowsAffected()
} }

113
store/player_test.go Normal file
View file

@ -0,0 +1,113 @@
package store_test
import (
"testing"
"time"
"github.com/genudine/saerro-go/store"
"github.com/genudine/saerro-go/types"
"github.com/stretchr/testify/assert"
)
func TestPlayerMigrationClean(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewPlayerStore(db)
isMigrated := ps.IsMigrated(ctx)
assert.True(t, isMigrated)
}
func TestPlayerMultipleStartups(t *testing.T) {
ctx, db := mkEnv(t)
ps1 := store.NewPlayerStore(db)
ps2 := store.NewPlayerStore(db)
isMigrated := ps2.IsMigrated(ctx)
assert.True(t, isMigrated)
assert.True(t, ps1.RanMigration)
assert.False(t, ps2.RanMigration)
}
func TestPlayerMigrationRerun(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewPlayerStore(db)
ps.RunMigration(ctx, true)
isMigrated := ps.IsMigrated(ctx)
assert.True(t, isMigrated)
}
func TestPlayerInsertGetOne(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewPlayerStore(db)
player := &types.Player{
CharacterID: "41666",
WorldID: 17,
FactionID: types.TR,
ZoneID: 4,
ClassName: "light_assault",
}
err := ps.Insert(ctx, player)
assert.NoError(t, err, "Insert failed")
p1, err := ps.GetOne(ctx, "41666")
assert.NoError(t, err, "GetOne failed")
assert.Equal(t, "light_assault", p1.ClassName)
time.Sleep(time.Second * 1)
player.ClassName = "combat_medic"
player.LastUpdated = time.Time{}
err = ps.Insert(ctx, player)
assert.NoError(t, err, "Insert failed")
p2, err := ps.GetOne(ctx, "41666")
assert.NoError(t, err, "GetOne failed")
assert.Equal(t, "combat_medic", p2.ClassName)
assert.NotEqual(t, p1.LastUpdated, p2.LastUpdated, "time did not update as expected")
}
func TestPlayerPrune(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewPlayerStore(db)
prunedPlayer := &types.Player{
CharacterID: "20155",
WorldID: 17,
FactionID: types.NC,
ZoneID: 4,
ClassName: "light_assault",
LastUpdated: time.Now().Add(-time.Minute * 20),
}
survivingPlayer := &types.Player{
CharacterID: "41666",
WorldID: 17,
FactionID: types.TR,
ZoneID: 4,
ClassName: "light_assault",
LastUpdated: time.Now().Add(-time.Minute * 5),
}
err := ps.Insert(ctx, prunedPlayer)
assert.NoError(t, err, "Insert prunedPlayer failed")
err = ps.Insert(ctx, survivingPlayer)
assert.NoError(t, err, "Insert survivingPlayer failed")
removed, err := ps.Prune(ctx)
assert.NoError(t, err, "Prune failed")
assert.Equal(t, int64(1), removed, "Prune count incorrect")
_, err = ps.GetOne(ctx, prunedPlayer.CharacterID)
assert.Error(t, err, "GetOne prunedPlayer failed, as expected.")
_, err = ps.GetOne(ctx, survivingPlayer.CharacterID)
assert.NoError(t, err, "GetOne survivingPlayer failed")
}

View file

@ -0,0 +1,45 @@
package storemock
import (
"context"
"database/sql"
"github.com/genudine/saerro-go/types"
"github.com/stretchr/testify/mock"
)
type MockPlayerStore struct {
mock.Mock
DB *sql.DB
RanMigration bool
}
func (m *MockPlayerStore) IsMigrated(ctx context.Context) bool {
args := m.Called(ctx)
return args.Bool(0)
}
func (m *MockPlayerStore) RunMigration(ctx context.Context, force bool) {
m.Called(ctx, force)
}
func (m *MockPlayerStore) Insert(ctx context.Context, player *types.Player) error {
args := m.Called(ctx, player)
return args.Error(0)
}
func (m *MockPlayerStore) GetOne(ctx context.Context, id string) (*types.Player, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*types.Player), args.Error(1)
}
func (m *MockPlayerStore) Prune(ctx context.Context) (int64, error) {
args := m.Called(ctx)
return int64(args.Int(0)), args.Error(1)
}

149
store/vehicle.go Normal file
View file

@ -0,0 +1,149 @@
package store
import (
"context"
"database/sql"
"log"
"time"
"github.com/genudine/saerro-go/types"
"github.com/avast/retry-go"
)
type VehicleStore struct {
DB *sql.DB
// Test introspection for if migrations ran during this PlayerStore init
RanMigration bool
}
func NewVehicleStore(db *sql.DB) *VehicleStore {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
ps := &VehicleStore{
DB: db,
}
ps.RunMigration(ctx, false)
return ps
}
func (ps *VehicleStore) IsMigrated(ctx context.Context) bool {
_, err := ps.DB.QueryContext(ctx, `SELECT count(1) FROM vehicles LIMIT 1;`)
if err != nil {
log.Printf("IsMigrated check failed: %v", err)
return false
}
return true
}
func (ps *VehicleStore) RunMigration(ctx context.Context, force bool) {
if !force && ps.IsMigrated(ctx) {
return
}
log.Println("(Re)-creating vehicles table")
ps.DB.ExecContext(ctx, `
DROP TABLE IF EXISTS vehicles;
CREATE TABLE vehicles (
character_id TEXT NOT NULL PRIMARY KEY,
last_updated TIMESTAMP NOT NULL,
world_id INT NOT NULL,
faction_id INT NOT NULL,
zone_id INT NOT NULL,
vehicle_name TEXT NOT NULL
);
-- TODO: Add indexes?
`)
log.Println("Done, vehicles table is initialized.")
ps.RanMigration = true
}
// Insert a player into the store.
// For testing, when LastUpdated is not "zero", the provided timestamp will be carried into the store.
func (ps *VehicleStore) Insert(ctx context.Context, vehicle *types.Vehicle) error {
if vehicle.LastUpdated.IsZero() {
vehicle.LastUpdated = time.Now()
}
err := retry.Do(func() error {
_, err := ps.DB.ExecContext(ctx,
`
INSERT INTO vehicles (
last_updated, character_id, world_id, faction_id, zone_id, vehicle_name
)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (character_id) DO UPDATE SET
last_updated = EXCLUDED.last_updated,
world_id = EXCLUDED.world_id,
faction_id = EXCLUDED.faction_id,
zone_id = EXCLUDED.zone_id,
vehicle_name = EXCLUDED.vehicle_name
`,
vehicle.LastUpdated,
vehicle.CharacterID,
vehicle.WorldID,
vehicle.FactionID,
vehicle.ZoneID,
vehicle.VehicleName,
)
return err
}, retry.Attempts(2))
return err
}
// GetOne player from the store.
func (ps *VehicleStore) GetOne(ctx context.Context, id string) (*types.Vehicle, error) {
row := ps.DB.QueryRowContext(ctx, `
SELECT
last_updated,
character_id,
world_id,
faction_id,
zone_id,
vehicle_name
FROM vehicles WHERE character_id = $1
`, id)
vehicle := &types.Vehicle{}
var timestamp string
err := row.Scan(
&timestamp,
&vehicle.CharacterID,
&vehicle.WorldID,
&vehicle.FactionID,
&vehicle.ZoneID,
&vehicle.VehicleName,
)
if err != nil {
return nil, err
}
vehicle.LastUpdated, err = time.Parse(time.RFC3339, timestamp)
return vehicle, err
}
func (ps *VehicleStore) Prune(ctx context.Context) (int64, error) {
log.Println("pruning VehicleStore")
// Avoid using sql idioms here for portability
// SQLite and PgSQL do now() differently, we don't need to at all.
res, err := ps.DB.ExecContext(ctx, `
DELETE FROM vehicles WHERE last_updated < $1;
`, time.Now().Add(-time.Minute*15))
if err != nil {
return 0, err
}
return res.RowsAffected()
}

114
store/vehicle_test.go Normal file
View file

@ -0,0 +1,114 @@
package store_test
import (
"testing"
"time"
"github.com/genudine/saerro-go/store"
"github.com/genudine/saerro-go/types"
"github.com/stretchr/testify/assert"
)
func TestVehicleMigrationClean(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewVehicleStore(db)
isMigrated := ps.IsMigrated(ctx)
assert.True(t, isMigrated)
}
func TestVehicleMultipleStartups(t *testing.T) {
ctx, db := mkEnv(t)
ps1 := store.NewVehicleStore(db)
ps2 := store.NewVehicleStore(db)
isMigrated := ps2.IsMigrated(ctx)
assert.True(t, isMigrated)
assert.True(t, ps1.RanMigration)
assert.False(t, ps2.RanMigration)
}
func TestVehicleMigrationRerun(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewVehicleStore(db)
ps.RunMigration(ctx, true)
isMigrated := ps.IsMigrated(ctx)
assert.True(t, isMigrated)
}
func TestVehicleInsertGetOne(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewVehicleStore(db)
player := &types.Vehicle{
CharacterID: "41666",
WorldID: 17,
FactionID: types.TR,
ZoneID: 4,
VehicleName: "harasser",
}
err := ps.Insert(ctx, player)
assert.NoError(t, err, "Insert failed")
p1, err := ps.GetOne(ctx, "41666")
assert.NoError(t, err, "GetOne failed")
assert.Equal(t, "harasser", p1.VehicleName)
time.Sleep(time.Second * 1)
player.VehicleName = "vanguard"
player.LastUpdated = time.Time{}
err = ps.Insert(ctx, player)
assert.NoError(t, err, "Insert failed")
p2, err := ps.GetOne(ctx, "41666")
assert.NoError(t, err, "GetOne failed")
assert.Equal(t, "vanguard", p2.VehicleName)
assert.NotEqual(t, p1.LastUpdated, p2.LastUpdated, "time did not update as expected")
}
func TestVehiclePrune(t *testing.T) {
ctx, db := mkEnv(t)
ps := store.NewVehicleStore(db)
prunedPlayer := &types.Vehicle{
CharacterID: "20155",
WorldID: 17,
FactionID: types.NC,
ZoneID: 4,
VehicleName: "harasser",
LastUpdated: time.Now().Add(-time.Minute * 20),
}
survivingPlayer := &types.Vehicle{
CharacterID: "41666",
WorldID: 17,
FactionID: types.TR,
ZoneID: 4,
VehicleName: "harasser",
LastUpdated: time.Now().Add(-time.Minute * 5),
}
err := ps.Insert(ctx, prunedPlayer)
assert.NoError(t, err, "Insert prunedPlayer failed")
err = ps.Insert(ctx, survivingPlayer)
assert.NoError(t, err, "Insert survivingPlayer failed")
removed, err := ps.Prune(ctx)
assert.NoError(t, err, "Prune failed")
assert.Equal(t, int64(1), removed, "Prune count incorrect")
_, err = ps.GetOne(ctx, prunedPlayer.CharacterID)
assert.Error(t, err, "GetOne prunedPlayer failed, as expected.")
_, err = ps.GetOne(ctx, survivingPlayer.CharacterID)
assert.NoError(t, err, "GetOne survivingPlayer failed")
}

View file

@ -12,40 +12,40 @@ const (
) )
var ( var (
LoadoutMap = map[string]Class{ LoadoutMap = map[uint16]Class{
"1": Infiltrator, 1: Infiltrator,
"8": Infiltrator, 8: Infiltrator,
"15": Infiltrator, 15: Infiltrator,
"28": Infiltrator, 28: Infiltrator,
"3": LightAssault, 3: LightAssault,
"10": LightAssault, 10: LightAssault,
"17": LightAssault, 17: LightAssault,
"29": LightAssault, 29: LightAssault,
"4": CombatMedic, 4: CombatMedic,
"11": CombatMedic, 11: CombatMedic,
"18": CombatMedic, 18: CombatMedic,
"30": CombatMedic, 30: CombatMedic,
"5": Engineer, 5: Engineer,
"12": Engineer, 12: Engineer,
"19": Engineer, 19: Engineer,
"31": Engineer, 31: Engineer,
"6": HeavyAssault, 6: HeavyAssault,
"13": HeavyAssault, 13: HeavyAssault,
"20": HeavyAssault, 20: HeavyAssault,
"32": HeavyAssault, 32: HeavyAssault,
"7": MAX, 7: MAX,
"14": MAX, 14: MAX,
"21": MAX, 21: MAX,
"45": MAX, 45: MAX,
} }
) )
func ClassFromLoadout(loadoutID string) Class { func ClassFromLoadout(loadoutID uint16) Class {
c, ok := LoadoutMap[loadoutID] c, ok := LoadoutMap[loadoutID]
if !ok { if !ok {

View file

@ -3,11 +3,11 @@ package translators_test
import ( import (
"testing" "testing"
"github.com/genudine/saerro-go/pkg/translators" "github.com/genudine/saerro-go/translators"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestLoadouts(t *testing.T) { func TestLoadouts(t *testing.T) {
assert.Equal(t, translators.ClassFromLoadout("1"), translators.Infiltrator) assert.Equal(t, translators.ClassFromLoadout(1), translators.Infiltrator)
assert.Equal(t, translators.ClassFromLoadout("0"), translators.Class("unknown")) assert.Equal(t, translators.ClassFromLoadout(0), translators.Class("unknown"))
} }

View file

@ -3,7 +3,7 @@ package translators_test
import ( import (
"testing" "testing"
"github.com/genudine/saerro-go/pkg/translators" "github.com/genudine/saerro-go/translators"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View file

@ -8,3 +8,18 @@ const (
TR TR
NSO NSO
) )
// func (f Faction) UnmarshalJSON(b []byte) error {
// switch b[0] {
// case '1':
// f = VS
// case '2':
// f = NC
// case '3':
// f = TR
// case '4':
// f = NSO
// }
// return nil
// }

27
types/internal.go Normal file
View file

@ -0,0 +1,27 @@
package types
import "time"
type Player struct {
CharacterID string `json:"character_id"`
LastUpdated time.Time `json:"last_updated"`
WorldID uint16 `json:"world_id"`
FactionID Faction `json:"faction_id"`
ZoneID uint32 `json:"zone_id"`
ClassName string `json:"class_name"`
}
type Vehicle struct {
CharacterID string `json:"character_id"`
LastUpdated time.Time `json:"last_updated"`
WorldID uint16 `json:"world_id"`
FactionID Faction `json:"faction_id"`
ZoneID uint32 `json:"zone_id"`
VehicleName string `json:"vehicle_name"`
}
type AnalyticEvent struct {
Time time.Time `json:"time"`
WorldID uint16 `json:"world_id"`
EventName string `json:"event_name"`
}

View file

@ -6,18 +6,23 @@ type ESSData struct {
type ESSEvent struct { type ESSEvent struct {
EventName string `json:"event_name"` EventName string `json:"event_name"`
WorldID uint16 `json:"world_id"` WorldID uint16 `json:"world_id,string"`
ZoneID uint32 `json:"zone_id"` ZoneID uint32 `json:"zone_id,string"`
CharacterID string `json:"character_id"` CharacterID string `json:"character_id"`
LoadoutID string `json:"loadout_id"`
VehicleID string `json:"vehicle_id"`
TeamID Faction `json:"team_id"`
// On Death
VehicleID string `json:"vehicle_id"`
TeamID Faction `json:"team_id,string"`
CharacterLoadoutID uint16 `json:"character_loadout_id,string"`
AttackerCharacterID string `json:"attacker_character_id"` AttackerCharacterID string `json:"attacker_character_id"`
AttackerLoadoutID string `json:"attacker_loadout_id"` AttackerLoadoutID uint16 `json:"attacker_loadout_id,string"`
AttackerVehicleID string `json:"attacker_vehicle_id"` AttackerVehicleID string `json:"attacker_vehicle_id"`
AttackerTeamID Faction `json:"attacker_team_id"` AttackerTeamID Faction `json:"attacker_team_id,string"`
ExperienceID uint32 // On GainExperience
ExperienceID uint32 `json:"experience_id,string"`
LoadoutID uint16 `json:"loadout_id,string"`
} }

View file

@ -1,23 +1,23 @@
package main // TODO: tests
package types
import ( import (
"github.com/genudine/saerro-go/translators" "github.com/genudine/saerro-go/translators"
"github.com/genudine/saerro-go/types"
) )
type PopEvent struct { type PopEvent struct {
WorldID uint16 WorldID uint16
ZoneID uint32 ZoneID uint32
CharacterID string CharacterID string
LoadoutID string LoadoutID uint16
TeamID types.Faction TeamID Faction
VehicleID string VehicleID string
VehicleName translators.Vehicle VehicleName translators.Vehicle
ClassName translators.Class ClassName translators.Class
} }
func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent { func PopEventFromESSEvent(event ESSEvent, attacker bool) PopEvent {
pe := PopEvent{ pe := PopEvent{
WorldID: event.WorldID, WorldID: event.WorldID,
ZoneID: event.ZoneID, ZoneID: event.ZoneID,
@ -25,7 +25,7 @@ func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent {
if !attacker { if !attacker {
pe.CharacterID = event.CharacterID pe.CharacterID = event.CharacterID
pe.LoadoutID = event.LoadoutID pe.LoadoutID = event.CharacterLoadoutID
pe.TeamID = event.TeamID pe.TeamID = event.TeamID
pe.VehicleID = event.VehicleID pe.VehicleID = event.VehicleID
} else { } else {
@ -35,8 +35,22 @@ func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent {
pe.VehicleID = event.AttackerVehicleID pe.VehicleID = event.AttackerVehicleID
} }
if pe.LoadoutID == 0 {
pe.LoadoutID = event.LoadoutID
}
pe.ClassName = translators.ClassFromLoadout(pe.LoadoutID) pe.ClassName = translators.ClassFromLoadout(pe.LoadoutID)
pe.VehicleName = translators.VehicleNameFromID(pe.VehicleID) pe.VehicleName = translators.VehicleNameFromID(pe.VehicleID)
return pe return pe
} }
func (pe PopEvent) ToPlayer() *Player {
return &Player{
CharacterID: pe.CharacterID,
ClassName: string(pe.ClassName),
FactionID: pe.TeamID,
ZoneID: pe.ZoneID,
WorldID: pe.WorldID,
}
}

72
types/pop_event_test.go Normal file
View file

@ -0,0 +1,72 @@
package types_test
import (
"testing"
"github.com/genudine/saerro-go/types"
"github.com/stretchr/testify/assert"
)
var (
deathEvent = types.ESSEvent{
EventName: "Death",
WorldID: 17,
ZoneID: 4,
CharacterID: "LyytisDoll",
TeamID: types.NC,
CharacterLoadoutID: 4,
AttackerCharacterID: "Lyyti",
AttackerTeamID: types.TR,
AttackerLoadoutID: 3,
}
expEvent = types.ESSEvent{
EventName: "GainExperience",
WorldID: 17,
ZoneID: 4,
CharacterID: "LyytisDoll",
TeamID: types.NC,
LoadoutID: 3,
ExperienceID: 55,
}
)
func TestPopEventFromDeath(t *testing.T) {
victim := types.PopEventFromESSEvent(deathEvent, false)
assert.Equal(t, deathEvent.CharacterID, victim.CharacterID)
assert.Equal(t, deathEvent.TeamID, victim.TeamID)
assert.Equal(t, deathEvent.CharacterLoadoutID, victim.LoadoutID)
assert.Equal(t, "combat_medic", string(victim.ClassName))
attacker := types.PopEventFromESSEvent(deathEvent, true)
assert.Equal(t, deathEvent.AttackerCharacterID, attacker.CharacterID)
assert.Equal(t, deathEvent.AttackerTeamID, attacker.TeamID)
assert.Equal(t, deathEvent.AttackerLoadoutID, attacker.LoadoutID)
assert.Equal(t, "light_assault", string(attacker.ClassName))
}
func TestPopEventFromExperienceGain(t *testing.T) {
pe := types.PopEventFromESSEvent(expEvent, false)
assert.Equal(t, expEvent.CharacterID, pe.CharacterID)
assert.Equal(t, expEvent.TeamID, pe.TeamID)
assert.Equal(t, expEvent.LoadoutID, pe.LoadoutID)
assert.Equal(t, "light_assault", string(pe.ClassName))
}
func TestPopEventFromVehicleDestroy(t *testing.T) {
t.SkipNow()
}
func TestPopEventToPlayer(t *testing.T) {
pe := types.PopEventFromESSEvent(deathEvent, false)
player := pe.ToPlayer()
assert.Equal(t, pe.CharacterID, player.CharacterID)
assert.Equal(t, pe.TeamID, player.FactionID)
assert.Equal(t, pe.ZoneID, player.ZoneID)
assert.Equal(t, string(pe.ClassName), player.ClassName)
assert.Equal(t, pe.WorldID, player.WorldID)
}

22
util/db_connector.go Normal file
View file

@ -0,0 +1,22 @@
package util
import (
"database/sql"
"fmt"
_ "github.com/jackc/pgx/v5/stdlib"
)
func GetDBConnection(addr string) (*sql.DB, error) {
db, err := sql.Open("pgx", addr)
if err != nil {
return nil, fmt.Errorf("db failed to open, %w", err)
}
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("db failed to ping, %w", err)
}
return db, nil
}

20
util/testutil/db.go Normal file
View file

@ -0,0 +1,20 @@
package testutil
import (
"database/sql"
"testing"
_ "modernc.org/sqlite"
)
// GetTestDB standardizes what type of DB tests use.
func GetTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", t.TempDir()+"/test.db")
if err != nil {
t.Fatalf("test shim: sqlite open failed, %v", err)
}
return db
}

50
util/testutil/ws.go Normal file
View file

@ -0,0 +1,50 @@
package testutil
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/coder/websocket"
)
type MockESS struct {
Server *httptest.Server
LastMessage string
}
func (m MockESS) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
c, err := websocket.Accept(rw, req, nil)
if err != nil {
rw.WriteHeader(400)
rw.Write([]byte("websocket connection failed"))
return
}
defer c.CloseNow()
ctx, cancel := context.WithTimeout(req.Context(), time.Second*30)
defer cancel()
_, body, err := c.Read(ctx)
if err != nil {
rw.WriteHeader(500)
rw.Write([]byte("websocket read failed"))
return
}
m.LastMessage = string(body)
}
func GetMockESS(t *testing.T) MockESS {
t.Helper()
m := MockESS{}
s := httptest.NewServer(m)
m.Server = s
t.Cleanup(s.Close)
return m
}

8
util/timestrings.go Normal file
View file

@ -0,0 +1,8 @@
package util
import "time"
// Makes times compatible with old Saerro API
func TimeToString(t time.Time) string {
return t.UTC().Format(time.RFC3339)
}