138 lines
4.3 KiB
Go
138 lines
4.3 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
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|