Scert/backend/internal/deployment/recovery_confirmation_test.go

98 lines
3.3 KiB
Go

package deployment
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
func confirmationFixture(t *testing.T) (*Service, RecoveryConfirmation, string) {
t.Helper()
server, client, record := recoveryPreflightFixture()
service := NewServiceWithJournal(fixedClientFactory{client}, &memoryTransactionJournal{pending: []TransactionRecord{record}})
confirmation, token, err := service.PrepareRecoveryConfirmation(t.Context(), record.ID, "unix:0", RecoveryActionRecord, func(context.Context, string) (Server, error) { return server, nil })
if err != nil {
t.Fatal(err)
}
return service, confirmation, token
}
func TestRecoveryConfirmationSingleUseAndPrivateBindings(t *testing.T) {
service, confirmation, token := confirmationFixture(t)
if len(token) != 64 || len(confirmation.DispositionID()) != 64 || token == confirmation.DispositionID() {
t.Fatal("invalid confirmation entropy")
}
encoded, err := json.Marshal(confirmation)
if err != nil || string(encoded) != "{}" {
t.Fatal("private confirmation serialized")
}
copy := confirmation
review, intent, err := service.consumeRecoveryConfirmation(t.Context(), confirmation, "unix:0", token)
if err != nil || review.Summary() != confirmation.Summary() || intent.Validate() != nil || intent.ID != confirmation.DispositionID() {
t.Fatalf("confirmation failed: %v", err)
}
encoded, _ = json.Marshal(intent)
if strings.Contains(string(encoded), token) {
t.Fatal("raw token persisted")
}
if _, _, err := service.consumeRecoveryConfirmation(t.Context(), copy, "unix:0", token); !errors.Is(err, ErrRecoveryConfirmation) {
t.Fatal("copied confirmation replayed")
}
}
func TestRecoveryConfirmationRejectsAndConsumesInvalidAttempt(t *testing.T) {
for _, scenario := range []string{"identity", "token", "oversized", "expired", "service", "cancelled", "empty"} {
t.Run(scenario, func(t *testing.T) {
service, confirmation, token := confirmationFixture(t)
originalService, originalToken := service, token
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
operator := "unix:0"
switch scenario {
case "identity":
operator = "unix:1"
case "token":
token = strings.Repeat("0", 64)
case "oversized":
token = strings.Repeat("a", 8192)
case "expired":
service.now = func() time.Time { return confirmation.Summary().ExpiresAt }
case "service":
service = NewService(nil)
case "cancelled":
cancel()
case "empty":
confirmation = RecoveryConfirmation{}
}
if _, _, err := service.consumeRecoveryConfirmation(ctx, confirmation, operator, token); err == nil {
t.Fatal("invalid confirmation accepted")
}
if _, _, err := originalService.consumeRecoveryConfirmation(t.Context(), confirmation, "unix:0", originalToken); err == nil {
t.Fatal("failed attempt could be replayed")
}
})
}
}
func TestRecoveryConfirmationConcurrentCopiesConsumeOnlyOnce(t *testing.T) {
service, confirmation, token := confirmationFixture(t)
var accepted atomic.Int32
var group sync.WaitGroup
for range 32 {
group.Go(func() {
if _, _, err := service.consumeRecoveryConfirmation(t.Context(), confirmation, "unix:0", token); err == nil {
accepted.Add(1)
}
})
}
group.Wait()
if accepted.Load() != 1 {
t.Fatalf("accepted %d confirmations", accepted.Load())
}
}