package main import ( "encoding/json" "net/http" "strings" "testing" ) // The Go derivation must match crypto.js deriveInvite exactly, or a link minted // by add-instance would be unclaimable in the browser. The expected values are a // vector computed by the real crypto.js (node, WebCrypto) for T = bytes 0x00..0x0f. func TestDeriveInvitePartsMatchesCryptoJS(t *testing.T) { id, claimSecret, claimHash, err := deriveInviteParts("AAECAwQFBgcICQoLDA0ODw") if err != nil { t.Fatalf("derive: %v", err) } if id != "5ReoOHe_g9MBI9A925thvq4BM7NNXPgx3tOzReMa6A4" { t.Fatalf("id mismatch: %s", id) } if claimSecret != "Kog7DRX7AZVY-PF5H-8FjH7Oc6BOPacaZjLLRzJ9Q_g" { t.Fatalf("claimSecret mismatch: %s", claimSecret) } if claimHash != "79b97fffe2f562bc4a5c6e6ab76147447aff96fd553fc4b1cd6cb83a537c0caa" { t.Fatalf("claimHash mismatch: %s", claimHash) } } // mintSetupInvite against a real (test) instance produces a link whose fragment // secret derives to a claimable, admin-granting invite — the whole add-instance → // browser handshake in one test. func TestMintSetupInviteEndToEnd(t *testing.T) { ts, srv := newTestServer(t) link, err := mintSetupInvite(http.DefaultClient, ts.URL, "https://canary.example", "admintok") if err != nil { t.Fatalf("mint: %v", err) } prefix := "https://canary.example/join/setup#" if !strings.HasPrefix(link, prefix) { t.Fatalf("link shape: %s", link) } tB64 := strings.TrimPrefix(link, prefix) // The browser side: derive from the fragment, inspect, claim. id, claimSecret, _, err := deriveInviteParts(tB64) if err != nil { t.Fatalf("derive: %v", err) } var info struct { OK bool `json:"ok"` GrantsAdmin bool `json:"grants_admin"` } resp, _ := http.Get(ts.URL + "/api/key-invite/" + id) json.NewDecoder(resp.Body).Decode(&info) if !info.OK || !info.GrantsAdmin { t.Fatalf("inspect: %+v", info) } // An admin-granting link must not be claimable forever: minting sets a // real expiry (a stale link in scrollback/logs shouldn't mint an admin). ki, found, _ := srv.db.getKeyInvite(id) if !found { t.Fatalf("invite not found") } if ki.Expired { t.Fatalf("fresh setup invite should not be expired") } var expiresAt int64 srv.db.sql.QueryRow("SELECT COALESCE(expires_at, 0) FROM key_invites WHERE id = ?", id).Scan(&expiresAt) if expiresAt == 0 { t.Fatalf("setup invite has no expiry — a stale admin-granting link would live forever") } tok, ok, err := srv.db.claimKeyInvite(id, "", "", claimSecret) if err != nil || !ok || tok == "" { t.Fatalf("claim: ok=%v err=%v", ok, err) } uid, _ := srv.db.deviceTokenUser(tok) if srv.db.userRole(uid) != "admin" { t.Fatalf("claimer should be admin") } } // A rejected mint (wrong token) surfaces the HTTP failure rather than printing a // dead link. func TestMintSetupInviteAuthFailure(t *testing.T) { ts, _ := newTestServer(t) if _, err := mintSetupInvite(http.DefaultClient, ts.URL, ts.URL, "wrong"); err == nil { t.Fatalf("want error on bad admin token") } }