98 lines
3.3 KiB
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())
|
|
}
|
|
}
|