Its own store, its own test database, the same shape every other context has. Five properties: a key with nobody to seal it to is refused rather than kept readably; a key is sealed once per holder and the blobs differ because they are sealed to different machines; a holder recorded afterwards has none and the existing ones keep theirs; releasing a consumer takes its key; and a licence nobody recorded is refused by name. The last was the only one whose message mattered and whose message was not checked — the database's own foreign-key error is true and mentions a constraint, which sends somebody to read a schema instead of typing the name they meant. Partial sealing now says how far it got. The person holding the key is the only one who can finish, and running it again knowing what it will do is different from running it hoping.
83 lines
2.0 KiB
Go
83 lines
2.0 KiB
Go
package licences
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/novox/mesh-control/internal/store"
|
|
)
|
|
|
|
// ForTest is a fresh licence store in a database of its own, dropped when the test ends.
|
|
//
|
|
// The same shape inventory's has, and separate for the same reason the contexts are separate:
|
|
// each owns its store, including in a test.
|
|
func ForTest(t *testing.T) *Licences {
|
|
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("lic_%d_%s", time.Now().UnixNano()%1_000_000,
|
|
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
|
|
if len(name) > 60 {
|
|
name = name[:60]
|
|
}
|
|
|
|
conn, err := pgx.Connect(t.Context(), admin)
|
|
if err != nil {
|
|
t.Fatalf("cannot reach the test PostgreSQL: %v", err)
|
|
}
|
|
if _, err := conn.Exec(t.Context(), "create database "+name); err != nil {
|
|
t.Fatalf("cannot create %s: %v", name, err)
|
|
}
|
|
conn.Close(t.Context())
|
|
|
|
cut := strings.LastIndex(admin, "/")
|
|
t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable")
|
|
|
|
held, err := Open(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
held.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 := held.Ready(t.Context(), 20*time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrations, err := Migrations()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := held.store.Migrate(t.Context(), migrations); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return held
|
|
}
|
|
|
|
// ASealingKey is a public key something can be sealed to.
|
|
func ASealingKey(t *testing.T) string {
|
|
t.Helper()
|
|
k, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes())
|
|
}
|