ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
145 lines
4.6 KiB
Go
145 lines
4.6 KiB
Go
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
|
|
purpose string
|
|
}
|
|
|
|
func (p *captureSMS) SendForPurpose(ctx context.Context, phone, code, id, purpose string) error {
|
|
p.purpose = purpose
|
|
return p.Send(ctx, phone, code, id)
|
|
}
|
|
|
|
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", CaptchaID: f.knownCaptchaFor(f.web.Hash), CaptchaCode: "654321"}
|
|
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")
|
|
}
|
|
in.CaptchaID = f.knownCaptchaFor(f.web.Hash)
|
|
_, 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")
|
|
}
|
|
}
|