161 lines
3.9 KiB
Go
161 lines
3.9 KiB
Go
package api_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"wh/api"
|
|
"wh/config"
|
|
"wh/runner"
|
|
"wh/sim"
|
|
"wh/store"
|
|
"wh/vm"
|
|
)
|
|
|
|
func TestEndToEnd(t *testing.T) {
|
|
ctx := context.Background()
|
|
cfg := config.Default()
|
|
cfg.AsteroidCount = 20
|
|
|
|
// Set WH_TEST_PG to a postgres:// URL to run this against PostgreSQL
|
|
// (its public schema is wiped first).
|
|
dsn := "sqlite:" + filepath.Join(t.TempDir(), "t.db")
|
|
if pg := os.Getenv("WH_TEST_PG"); pg != "" {
|
|
db, err := sql.Open("pgx", pg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.Close()
|
|
dsn = pg
|
|
}
|
|
st, err := store.Open(dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := httptest.NewServer(api.New(st, cfg))
|
|
defer srv.Close()
|
|
|
|
var key string
|
|
do := func(method, path string, body []byte, want int) []byte {
|
|
t.Helper()
|
|
req, _ := http.NewRequest(method, srv.URL+path, bytes.NewReader(body))
|
|
if key != "" {
|
|
req.Header.Set("Authorization", "Bearer "+key)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
b, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != want {
|
|
t.Fatalf("%s %s: status %d want %d: %s", method, path, resp.StatusCode, want, b)
|
|
}
|
|
return b
|
|
}
|
|
|
|
var reg struct {
|
|
APIKey string `json:"api_key"`
|
|
ShipID int64 `json:"ship_id"`
|
|
}
|
|
json.Unmarshal(do("POST", "/register", []byte(`{"name":"ann"}`), 201), ®)
|
|
do("POST", "/register", []byte(`{"name":"ann"}`), 409)
|
|
do("GET", "/me", nil, 401)
|
|
key = reg.APIKey
|
|
ship := "/ships/" + itoa(reg.ShipID)
|
|
|
|
// Launch needs a program first; program limits are enforced.
|
|
do("POST", ship+"/launch", nil, 409)
|
|
do("PUT", ship+"/program", []byte{1, 2, 3}, 400)
|
|
do("PUT", ship+"/program", make([]byte, cfg.ProgramBytes+4), 413)
|
|
|
|
// Echo the uplink back on the downlink, then idle.
|
|
prog, err := vm.Assemble(`
|
|
.equ TX 1024
|
|
in r1, 0x70
|
|
ldi r2, 0
|
|
beq r1, r2, idle
|
|
ldw r3, [r2]
|
|
stw r3, [r2+TX]
|
|
out 0x70, r1
|
|
idle:
|
|
yield
|
|
jmp idle
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
do("PUT", ship+"/program", prog, 204)
|
|
do("PUT", ship+"/uplink", []byte("hi"), 409) // not launched yet
|
|
do("POST", ship+"/launch", nil, 202)
|
|
do("PUT", ship+"/program", prog, 409) // frozen once launching
|
|
do("PUT", ship+"/uplink", []byte("PING"), 204)
|
|
do("PUT", ship+"/uplink", make([]byte, cfg.CommBytes+1), 413)
|
|
do("GET", ship+"/downlink", nil, 404)
|
|
|
|
res, err := runner.RunNextDay(ctx, st, cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Day != 0 {
|
|
t.Fatalf("first run should be day 0, got %d", res.Day)
|
|
}
|
|
if b := do("GET", ship+"/downlink", nil, 200); len(b) != cfg.CommBytes || string(b[:4]) != "PING" {
|
|
t.Fatalf("downlink = %q", b[:8])
|
|
}
|
|
var ships []struct{ Status string }
|
|
json.Unmarshal(do("GET", "/ships", nil, 200), &ships)
|
|
if len(ships) != 1 || ships[0].Status != store.StatusActive {
|
|
t.Fatalf("ships = %+v", ships)
|
|
}
|
|
do("GET", "/market", nil, 200)
|
|
|
|
// A second day from the persisted snapshot must match an uninterrupted
|
|
// in-memory run: persistence may not change the simulation.
|
|
res2, err := runner.RunNextDay(ctx, st, cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mem := sim.NewState(cfg)
|
|
in := sim.DayInput{
|
|
Launches: []sim.Launch{{ShipID: reg.ShipID, Owner: 1, Program: prog}},
|
|
Uplinks: []sim.Uplink{{ShipID: reg.ShipID, Data: []byte("PING")}},
|
|
}
|
|
var want sim.DayResult
|
|
for d := 0; d < 2; d++ {
|
|
if want, err = mem.RunDay(cfg, in); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in = sim.DayInput{}
|
|
}
|
|
if res2.Hash != want.Hash {
|
|
t.Fatal("state after DB round-trip differs from in-memory run")
|
|
}
|
|
|
|
// Changing the config on an existing world is refused.
|
|
cfg.Seed = 99
|
|
if _, err := runner.RunNextDay(ctx, st, cfg); err == nil {
|
|
t.Fatal("expected config mismatch error")
|
|
}
|
|
}
|
|
|
|
func itoa(n int64) string {
|
|
b, _ := json.Marshal(n)
|
|
return string(b)
|
|
}
|