Ctrl K
ComunidadGuías…

internal/app/auth/otp_delivery_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
	}
}