170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/novox/mesh-controller/internal/store"
|
|
)
|
|
|
|
// ForTest is a fresh inventory in a database of its own, dropped when the test ends.
|
|
//
|
|
// Exported because the check that this mesh reads what a node sends lives beside the link, and a
|
|
// second copy of this would be a second thing to keep true. It takes a *testing.T, so it cannot
|
|
// be called from anything that is not a test.
|
|
func ForTest(t *testing.T) *Inventory {
|
|
t.Helper()
|
|
admin := os.Getenv("MESH_TEST_POSTGRES")
|
|
if admin == "" {
|
|
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
|
|
}
|
|
name := fmt.Sprintf("inv_%d_%s", time.Now().UnixNano()%1_000_000,
|
|
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
|
|
if len(name) > 60 {
|
|
name = name[:60]
|
|
}
|
|
|
|
// Created already migrated, a copy of a template migrated once (novox/hq issue 591); the Migrate
|
|
// below then finds nothing pending, which it does only if the copy's record matches every migration.
|
|
migrations, err := Migrations()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store.CreateMigratedForTest(t, admin, name, migrations)
|
|
busPasswordCostForTest.Store(int32(bcrypt.MinCost))
|
|
|
|
cut := strings.LastIndex(admin, "/")
|
|
t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable")
|
|
|
|
inv, err := Open(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
inv.Close()
|
|
c, err := pgx.Connect(context.Background(), admin)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close(context.Background())
|
|
_, _ = c.Exec(context.Background(), "drop database if exists "+name+" with (force)")
|
|
})
|
|
if err := inv.Ready(t.Context(), 20*time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := inv.store.Migrate(t.Context(), migrations); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return inv
|
|
}
|
|
|
|
// RemoveNodeForTest removes a machine's record as a person removing it from the store would: no verb of the
|
|
// mesh removes one yet, and what hangs off it must go with it.
|
|
func (i *Inventory) RemoveNodeForTest(ctx context.Context, name string) (bool, error) {
|
|
tag, err := i.store.Pool().Exec(ctx, `delete from node where name = $1`, name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tag.RowsAffected() == 1, nil
|
|
}
|
|
|
|
// ForTestBefore is ForTest with the store migrated only below migration `number`, and a function that runs the
|
|
// rest: for a test of a migration that moves data, which seeds the store as the mesh holds it before the
|
|
// migration, migrates, and reads what the migration made of it (novox/hq issue 651).
|
|
func ForTestBefore(t *testing.T, number int) (*Inventory, func()) {
|
|
t.Helper()
|
|
admin := os.Getenv("MESH_TEST_POSTGRES")
|
|
if admin == "" {
|
|
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
|
|
}
|
|
name := fmt.Sprintf("invb_%d_%s", time.Now().UnixNano()%1_000_000,
|
|
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
|
|
if len(name) > 60 {
|
|
name = name[:60]
|
|
}
|
|
all, err := Migrations()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var before []store.Migration
|
|
for _, m := range all {
|
|
if m.Number < number {
|
|
before = append(before, m)
|
|
}
|
|
}
|
|
store.CreateMigratedForTest(t, admin, name, before)
|
|
busPasswordCostForTest.Store(int32(bcrypt.MinCost))
|
|
cut := strings.LastIndex(admin, "/")
|
|
t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable")
|
|
inv, err := Open(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
inv.Close()
|
|
c, err := pgx.Connect(context.Background(), admin)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close(context.Background())
|
|
_, _ = c.Exec(context.Background(), "drop database if exists "+name+" with (force)")
|
|
})
|
|
if err := inv.Ready(t.Context(), 20*time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return inv, func() {
|
|
t.Helper()
|
|
if _, err := inv.store.Migrate(t.Context(), all); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExecForTest runs one statement against a test's store, for a fixture no verb of the mesh writes: a row as an
|
|
// older migration left it.
|
|
func (i *Inventory) ExecForTest(t *testing.T, sql string, args ...any) {
|
|
t.Helper()
|
|
if _, err := i.store.Pool().Exec(t.Context(), sql, args...); err != nil {
|
|
t.Fatalf("%s: %v", sql, err)
|
|
}
|
|
}
|
|
|
|
// QueryForTest reads rows of a test's store, each row's columns as JSON text, for a test that checks what a
|
|
// migration left.
|
|
func (i *Inventory) QueryForTest(t *testing.T, sql string, args ...any) [][]string {
|
|
t.Helper()
|
|
rows, err := i.store.Pool().Query(t.Context(), sql, args...)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", sql, err)
|
|
}
|
|
defer rows.Close()
|
|
var out [][]string
|
|
for rows.Next() {
|
|
values, err := rows.Values()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
row := make([]string, len(values))
|
|
for k, v := range values {
|
|
row[k] = fmt.Sprint(v)
|
|
if b, err := json.Marshal(v); err == nil {
|
|
row[k] = string(b)
|
|
}
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|