189 lines
7.2 KiB
Go
189 lines
7.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/otpdelivery"
|
|
"telesrv/internal/store"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
type captureOTPSender struct {
|
|
requests []otpdelivery.Request
|
|
err error
|
|
before func()
|
|
}
|
|
|
|
func (s *captureOTPSender) Deliver(_ context.Context, req otpdelivery.Request) (otpdelivery.Result, error) {
|
|
if s.before != nil {
|
|
s.before()
|
|
}
|
|
s.requests = append(s.requests, req)
|
|
return otpdelivery.Result{ProviderMessageID: "capture-message"}, s.err
|
|
}
|
|
|
|
func TestWebhookPhoneLoginUsesRandomSMSCode(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
user, err := users.Create(ctx, domain.User{Phone: "15550009301", FirstName: "Webhook"})
|
|
if err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
codes := memory.NewCodeStore()
|
|
appDelivery := &captureLoginCodeDelivery{}
|
|
sender := &captureOTPSender{before: func() {
|
|
if len(appDelivery.requests) != 1 {
|
|
t.Fatalf("provider called before durable App-code: requests=%d", len(appDelivery.requests))
|
|
}
|
|
}}
|
|
svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "fixed-code-must-not-leak",
|
|
WithLoginCodeDelivery(appDelivery),
|
|
WithPhoneCodeDelivery(sender, 6))
|
|
|
|
hash, err := svc.SendCode(ctx, "+1 555 000 9301")
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
if hash == "" || len(sender.requests) != 1 {
|
|
t.Fatalf("hash=%q requests=%d", hash, len(sender.requests))
|
|
}
|
|
req := sender.requests[0]
|
|
if req.DeliveryID == "" || req.Purpose != otpdelivery.PurposeLoginSMS || req.Channel != otpdelivery.ChannelSMS ||
|
|
req.Recipient != "15550009301" || len(req.Code) != 6 || req.Code == "fixed-code-must-not-leak" || time.Until(req.ExpiresAt) < 4*time.Minute {
|
|
t.Fatalf("request = %+v", req)
|
|
}
|
|
if len(appDelivery.requests) != 1 || appDelivery.requests[0].PhoneCodeHash != hash || appDelivery.requests[0].Code != req.Code {
|
|
t.Fatalf("App-code delivery=%+v, want same hash/code as provider", appDelivery.requests)
|
|
}
|
|
rec, found, err := codes.Get(ctx, hash)
|
|
if err != nil || !found || rec.Code != req.Code || rec.DeliveryID != req.DeliveryID || rec.Channel != store.PhoneCodeChannelSMS {
|
|
t.Fatalf("stored code=%+v found=%v err=%v", rec, found, err)
|
|
}
|
|
delivery, found, err := svc.CodeDelivery(ctx, hash)
|
|
if err != nil || !found || delivery.Kind != domain.AuthCodeDeliverySMS || delivery.Length != 6 {
|
|
t.Fatalf("delivery=%+v found=%v err=%v", delivery, found, err)
|
|
}
|
|
got, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: [8]byte{3}}, req.Recipient, hash, req.Code)
|
|
if err != nil || needSignUp || got.ID != user.ID {
|
|
t.Fatalf("SignIn user=%+v needSignUp=%v err=%v", got, needSignUp, err)
|
|
}
|
|
}
|
|
|
|
func TestWebhookPhoneLoginCanonicalizesNationalTrunkBeforeOTP(t *testing.T) {
|
|
ctx := context.Background()
|
|
sender := &captureOTPSender{}
|
|
svc := NewService(
|
|
memory.NewUserStore(),
|
|
memory.NewAuthorizationStore(),
|
|
memory.NewCodeStore(),
|
|
nil,
|
|
nil,
|
|
"fixed-code-must-not-leak",
|
|
WithPhoneCodeDelivery(sender, 6),
|
|
)
|
|
|
|
hash, err := svc.SendCode(ctx, "+98 0998 167 9461")
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
if len(sender.requests) != 1 || sender.requests[0].Recipient != "989981679461" {
|
|
t.Fatalf("OTP requests = %+v, want canonical Iran recipient", sender.requests)
|
|
}
|
|
_, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{}, "989981679461", hash, sender.requests[0].Code)
|
|
if err != nil || !needSignUp {
|
|
t.Fatalf("SignIn canonical variant needSignUp=%v err=%v", needSignUp, err)
|
|
}
|
|
}
|
|
|
|
func TestWebhookExistingAccountRejectionKeepsDurableAppCode(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
user, err := users.Create(ctx, domain.User{Phone: "15550009305", FirstName: "Fallback"})
|
|
if err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
codes := memory.NewCodeStore()
|
|
appDelivery := &captureLoginCodeDelivery{}
|
|
sender := &captureOTPSender{err: &otpdelivery.RejectedError{StatusCode: 503, Code: "UNAVAILABLE", Retryable: true}}
|
|
var observed []error
|
|
svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345",
|
|
WithLoginCodeDelivery(appDelivery),
|
|
WithPhoneCodeDelivery(sender, 6),
|
|
WithOTPDeliveryFailureObserver(func(_ context.Context, _ otpdelivery.Request, err error) {
|
|
observed = append(observed, err)
|
|
}),
|
|
)
|
|
|
|
hash, err := svc.SendCode(ctx, user.Phone)
|
|
if err != nil || hash == "" {
|
|
t.Fatalf("SendCode hash=%q err=%v, want App fallback success", hash, err)
|
|
}
|
|
if len(sender.requests) != 1 || len(appDelivery.requests) != 1 || len(observed) != 1 {
|
|
t.Fatalf("provider=%d App=%d observed=%d, want 1/1/1", len(sender.requests), len(appDelivery.requests), len(observed))
|
|
}
|
|
rec, found, err := codes.Get(ctx, hash)
|
|
if err != nil || !found || rec.Code != appDelivery.requests[0].Code || rec.Code != sender.requests[0].Code {
|
|
t.Fatalf("code=%+v found=%v err=%v App=%+v provider=%+v", rec, found, err, appDelivery.requests, sender.requests)
|
|
}
|
|
}
|
|
|
|
func TestWebhookPhoneLoginExplicitRejectionRollsBackCode(t *testing.T) {
|
|
ctx := context.Background()
|
|
baseCodes := memory.NewCodeStore()
|
|
codes := &trackingCodeStore{CodeStore: baseCodes}
|
|
sender := &captureOTPSender{err: &otpdelivery.RejectedError{StatusCode: 503, Code: "UNAVAILABLE", Retryable: true}}
|
|
svc := NewService(memory.NewUserStore(), memory.NewAuthorizationStore(), codes, nil, nil, "12345",
|
|
WithPhoneCodeDelivery(sender, 5))
|
|
|
|
hash, err := svc.SendCode(ctx, "15550009302")
|
|
if hash != "" || err == nil || len(sender.requests) != 1 || codes.lastSetHash == "" {
|
|
t.Fatalf("hash=%q err=%v requests=%d set=%q", hash, err, len(sender.requests), codes.lastSetHash)
|
|
}
|
|
if _, found, getErr := baseCodes.Get(ctx, codes.lastSetHash); getErr != nil || found {
|
|
t.Fatalf("rejected code found=%v err=%v", found, getErr)
|
|
}
|
|
}
|
|
|
|
func TestWebhookPhoneLoginUnknownOutcomeKeepsUsableCode(t *testing.T) {
|
|
ctx := context.Background()
|
|
codes := memory.NewCodeStore()
|
|
sender := &captureOTPSender{err: &otpdelivery.OutcomeUnknownError{Cause: errors.New("response lost")}}
|
|
svc := NewService(memory.NewUserStore(), memory.NewAuthorizationStore(), codes, nil, nil, "12345",
|
|
WithPhoneCodeDelivery(sender, 5))
|
|
|
|
hash, err := svc.SendCode(ctx, "15550009303")
|
|
if err != nil || hash == "" || len(sender.requests) != 1 {
|
|
t.Fatalf("hash=%q err=%v requests=%d", hash, err, len(sender.requests))
|
|
}
|
|
rec, found, err := codes.Get(ctx, hash)
|
|
if err != nil || !found || rec.Code != sender.requests[0].Code {
|
|
t.Fatalf("unknown outcome code=%+v found=%v err=%v", rec, found, err)
|
|
}
|
|
}
|
|
|
|
func TestWebhookPhoneResendRotatesCodeAndDeliveryID(t *testing.T) {
|
|
ctx := context.Background()
|
|
sender := &captureOTPSender{}
|
|
codes := memory.NewCodeStore()
|
|
svc := NewService(memory.NewUserStore(), memory.NewAuthorizationStore(), codes, nil, nil, "12345",
|
|
WithPhoneCodeDelivery(sender, 6))
|
|
firstHash, err := svc.SendCode(ctx, "15550009304")
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
secondHash, err := svc.ResendCode(ctx, "15550009304", firstHash)
|
|
if err != nil {
|
|
t.Fatalf("ResendCode: %v", err)
|
|
}
|
|
if firstHash == secondHash || len(sender.requests) != 2 ||
|
|
sender.requests[0].DeliveryID == sender.requests[1].DeliveryID {
|
|
t.Fatalf("hashes=%q/%q requests=%+v", firstHash, secondHash, sender.requests)
|
|
}
|
|
if _, found, err := codes.Get(ctx, firstHash); err != nil || found {
|
|
t.Fatalf("old code found=%v err=%v", found, err)
|
|
}
|
|
}
|