package inventory import ( "context" "errors" "fmt" "os" "strings" "sync" "testing" "time" "github.com/jackc/pgx/v5" "github.com/novox/mesh-control/internal/store" ) // Against a real PostgreSQL, for the reason novox/hq ADR 0017 gives: what is being tested here is // that the database enforces what this code relies on it enforcing — a unique name, a token that // two racing redemptions cannot both spend, a cascade that leaves no token behind. A fake would // assert that the fake enforces them. func fresh(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] } 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") 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) } migrations, err := Migrations() if err != nil { t.Fatal(err) } if _, err := inv.store.Migrate(t.Context(), migrations); err != nil { t.Fatal(err) } return inv } func TestANodeRecordRoundTrips(t *testing.T) { inv := fresh(t) made, err := inv.AddNode(t.Context(), "workstation") if err != nil { t.Fatal(err) } found, err := inv.NodeByName(t.Context(), "workstation") if err != nil { t.Fatal(err) } if found.ID != made.ID { t.Errorf("added %s and found %s", made.ID, found.ID) } } func TestTwoNodesCannotShareAName(t *testing.T) { // A name is how a token is issued for a node. Two records with one name makes that command // ambiguous at the moment it grants access to the mesh. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "workstation"); err != nil { t.Fatal(err) } _, err := inv.AddNode(t.Context(), "workstation") if !errors.Is(err, ErrNameTaken) { t.Fatalf("a duplicate name gave %v; it must be a plain answer a person can act on", err) } } func TestAnUnknownNodeIsNotAnEmptyRecord(t *testing.T) { inv := fresh(t) _, err := inv.NodeByName(t.Context(), "never-existed") if !errors.Is(err, ErrNoSuchNode) { t.Fatalf("expected ErrNoSuchNode, got %v", err) } } func TestATokenIsRedeemableExactlyOnce(t *testing.T) { // "Useless once used" (novox/hq ADR 0004). Without it a token that leaked after a successful // join is a second machine's way in, and nothing would have noticed the first. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(t.Context(), "laptop", time.Hour) if err != nil { t.Fatal(err) } node, err := inv.Redeem(t.Context(), issued.Secret) if err != nil { t.Fatalf("a fresh token was refused: %v", err) } if node.Name != "laptop" { t.Errorf("redeemed a token for %q", node.Name) } if _, err := inv.Redeem(t.Context(), issued.Secret); !errors.Is(err, ErrTokenRefused) { t.Fatal("the same token was redeemed twice") } } func TestAnExpiredTokenIsRefused(t *testing.T) { // "Useless after it expires" — the other half, and the one nothing notices, because a token // ages out with nobody watching. It has to be read from the row rather than from a status // something would have had to write. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(t.Context(), "laptop", 40*time.Millisecond) if err != nil { t.Fatal(err) } time.Sleep(120 * time.Millisecond) if _, err := inv.Redeem(t.Context(), issued.Secret); !errors.Is(err, ErrTokenRefused) { t.Fatal("an expired token was accepted") } } func TestATokenWithNoLifetimeIsRefused(t *testing.T) { inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } if _, err := inv.IssueToken(t.Context(), "laptop", 0); err == nil { t.Fatal("a token that never expires was issued") } } func TestIssuingAgainInvalidatesTheOutstandingToken(t *testing.T) { // Two live tokens for one node record are two machines able to join as the same node, with // nothing downstream able to tell which was meant. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } first, err := inv.IssueToken(t.Context(), "laptop", time.Hour) if err != nil { t.Fatal(err) } second, err := inv.IssueToken(t.Context(), "laptop", time.Hour) if err != nil { t.Fatal(err) } if _, err := inv.Redeem(t.Context(), first.Secret); !errors.Is(err, ErrTokenRefused) { t.Error("the first token still worked after a second was issued") } if _, err := inv.Redeem(t.Context(), second.Secret); err != nil { t.Errorf("the newest token was refused: %v", err) } } func TestTheSecretIsNotStored(t *testing.T) { // A copy of this database must not be a set of working credentials. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(t.Context(), "laptop", time.Hour) if err != nil { t.Fatal(err) } var stored string if err := inv.store.Pool().QueryRow(t.Context(), `select secret from enrolment_token limit 1`).Scan(&stored); err != nil { t.Fatal(err) } if stored == issued.Secret { t.Fatal("the token secret is stored verbatim; this table would be a set of live credentials") } if strings.Contains(stored, issued.Secret) { t.Fatal("the stored value contains the secret") } } func TestTwoRedemptionsOfOneSecretCannotBothWin(t *testing.T) { // The check and the spend are one statement for this reason. Reading first and writing second // leaves a window where two machines both pass the check and both join as the same node. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(t.Context(), "laptop", time.Hour) if err != nil { t.Fatal(err) } var wg sync.WaitGroup results := make([]error, 8) for i := range results { wg.Add(1) go func(i int) { defer wg.Done() _, results[i] = inv.Redeem(context.Background(), issued.Secret) }(i) } wg.Wait() won := 0 for _, err := range results { if err == nil { won++ } } if won != 1 { t.Errorf("%d of 8 concurrent redemptions succeeded; exactly one may", won) } } func TestRemovingANodeTakesItsTokensWithIt(t *testing.T) { // A token outliving the record it was issued for is a right to join as nobody. inv := fresh(t) node, err := inv.AddNode(t.Context(), "laptop") if err != nil { t.Fatal(err) } if _, err := inv.IssueToken(t.Context(), "laptop", time.Hour); err != nil { t.Fatal(err) } if _, err := inv.store.Pool().Exec(t.Context(), `delete from node where id = $1`, node.ID); err != nil { t.Fatal(err) } var left int if err := inv.store.Pool().QueryRow(t.Context(), `select count(*) from enrolment_token`).Scan(&left); err != nil { t.Fatal(err) } if left != 0 { t.Errorf("%d token(s) outlived the node record they were issued for", left) } } func TestAnUnknownSecretIsRefusedTheSameWayAsAnExpiredOne(t *testing.T) { // One error for every reason. Somebody guessing must not learn which of their guesses was a // real token that had merely expired. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } expired, err := inv.IssueToken(t.Context(), "laptop", 30*time.Millisecond) if err != nil { t.Fatal(err) } time.Sleep(100 * time.Millisecond) _, unknownErr := inv.Redeem(t.Context(), "not-a-token-at-all") _, expiredErr := inv.Redeem(t.Context(), expired.Secret) if unknownErr == nil || expiredErr == nil { t.Fatal("one of them was accepted") } if unknownErr.Error() != expiredErr.Error() { t.Errorf("the two are distinguishable:\n unknown: %v\n expired: %v", unknownErr, expiredErr) } }