feat: complete device onboarding and mobile binding workflows
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-12 08:57:29 +08:00
parent 7846f30d0b
commit 98fb2965d0
116 changed files with 15669 additions and 64 deletions
+137
View File
@@ -0,0 +1,137 @@
package onboarding
import (
"context"
"crypto/sha256"
"encoding/json"
"github.com/google/uuid"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestIntegrationClaimReplayCannotReturnReplacement(t *testing.T) {
f := newFixture(t)
in := ClaimInput{ElderID: f.elder, Generation: f.d.Generation}
id := uuid.NewString()
first, err := f.s.Claim(f.ctx, f.web, id, requestJSON(in), in)
must(t, err)
cancel := CancelInput{ClaimID: first["claim_id"].(string)}
_, err = f.s.Cancel(f.ctx, f.web, uuid.NewString(), requestJSON(cancel), cancel)
must(t, err)
_, err = f.s.Claim(f.ctx, f.web, uuid.NewString(), requestJSON(in), in)
must(t, err)
_, err = f.s.Claim(f.ctx, f.web, id, requestJSON(in), in)
if err == nil {
t.Fatal("returned cancelled claim from cache")
}
}
func TestIntegrationConsumedHandoffCannotReplayReplacement(t *testing.T) {
f := newFixture(t)
var local string
must(t, f.s.pool.QueryRow(f.ctx, `SELECT local_setup_id::text FROM device_setup_session WHERE id=$1::uuid`, f.setup).Scan(&local))
verifier := randomToken(32)
sum := sha256.Sum256([]byte(verifier))
in := HandoffInput{SetupID: f.setup, LocalID: local, Challenge: encode64(sum[:])}
id := uuid.NewString()
first, err := f.s.Handoff(f.ctx, f.d, id, requestJSON(in), in)
must(t, err)
_, err = f.s.Exchange(f.ctx, ExchangeInput{Token: first["handoff_token"].(string), Verifier: verifier})
must(t, err)
_, err = f.s.Handoff(f.ctx, f.d, uuid.NewString(), requestJSON(in), in)
must(t, err)
_, err = f.s.Handoff(f.ctx, f.d, id, requestJSON(in), in)
if err == nil {
t.Fatal("returned consumed handoff")
}
}
type captureSMS struct {
code string
calls int
}
func (p *captureSMS) Send(_ context.Context, _ string, code string, _ string) error {
p.code = code
p.calls++
return nil
}
func TestIntegrationSMSLoginRateAndOneTimeCode(t *testing.T) {
f := newFixture(t)
sender := &captureSMS{}
f.s.options.SMS = sender
f.s.options.SMSLoginEnabled = true
// Start a fresh anonymous browser session, as a phone opened from the device.
_, err := f.s.pool.Exec(f.ctx, `UPDATE onboarding_web_session SET user_id=NULL,issuer=NULL,subject=NULL WHERE token_hash=$1`, f.web.Hash)
must(t, err)
f.web.UserID = ""
f.web.Issuer = ""
f.web.Subject = ""
phone := "+8613" + strings.ReplaceAll(uuid.NewString(), "-", "")[:9]
// Use decimal digits while keeping independent fixtures unique.
for i, c := range phone {
if c >= 'a' && c <= 'f' {
phone = phone[:i] + string('0'+(c-'a')) + phone[i+1:]
}
}
in := SMSSendInput{Phone: phone, Purpose: "LOGIN"}
id := uuid.NewString()
sent, err := f.s.SendSMS(f.ctx, f.web, id, in)
must(t, err)
_, err = f.s.SendSMS(f.ctx, f.web, id, in)
must(t, err)
if sender.calls != 1 {
t.Fatal("SMS retry sent twice")
}
_, err = f.s.SendSMS(f.ctx, f.web, uuid.NewString(), in)
if err == nil {
t.Fatal("resend cooldown ignored")
}
verify := SMSVerifyInput{ChallengeID: sent["sms_challenge_id"].(string), Code: sender.code}
bad := verify
bad.Code = "000000"
if bad.Code == sender.code {
bad.Code = "999999"
}
_, err = f.s.VerifySMS(f.ctx, f.web, bad)
if err == nil {
t.Fatal("incorrect SMS accepted")
}
credentials, err := f.s.VerifySMS(f.ctx, f.web, verify)
must(t, err)
principal, err := f.s.AccountPrincipal(f.ctx, credentials.AccountToken, credentials.CSRF, true)
must(t, err)
if principal.Issuer != "urn:platform:onboarding:sms" {
t.Fatal("incorrect SMS identity namespace")
}
_, err = f.s.VerifySMS(f.ctx, f.web, verify)
if err == nil {
t.Fatal("SMS consumed twice")
}
}
func TestHTTPSMSGatewayContract(t *testing.T) {
calls := 0
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if r.Header.Get("Authorization") != "Bearer test-token" || r.Header.Get("Idempotency-Key") != "test-request" {
t.Error("gateway authentication missing")
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Error(err)
}
if body["code"] != "123456" || body["phone_e164"] != "+8613800000000" {
t.Error("gateway payload incorrect")
}
w.WriteHeader(204)
}))
defer server.Close()
provider, err := NewHTTPSMSProvider(server.URL, "test-token")
must(t, err)
provider.(*HTTPSMSProvider).client = server.Client()
must(t, provider.Send(context.Background(), "+8613800000000", "123456", "test-request"))
if calls != 1 {
t.Fatal("unexpected request count")
}
}