Ctrl K
ComunidadGuías…

internal/app/wallet/filestore_test.go

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

Archivo original ↗Preguntar a KiBot
package wallet

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"telesrv/internal/domain"
	"testing"
	"time"
)

type balanceProvider struct{ err error }

func (p *balanceProvider) Perform(context.Context, string, *string, *string) (string, error) {
	return `{"ok":true,"result":"1234567890"}`, p.err
}

func signChallenge(t *testing.T, c domain.WalletChallenge, seed byte) ([]byte, []byte) {
	t.Helper()
	private := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32))
	public := []byte(private.Public().(ed25519.PublicKey))
	addr, err := InitialAddress(public, false)
	if err != nil {
		t.Fatal(err)
	}
	hash, err := ProofHash(0, addr.Data(), c.Domain, uint64(c.IssuedAt.Unix()), c.Payload)
	if err != nil {
		t.Fatal(err)
	}
	return public, ed25519.Sign(private, hash[:])
}

func TestFileWalletLifecycle(t *testing.T) {
	ctx := context.Background()
	dir := filepath.Join(t.TempDir(), "wallet")
	store, err := NewFileStore(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer func() { store.Close() }()
	provider := &balanceProvider{}
	svc := NewService(store, provider, "wallet.example")
	now := time.Now().UTC().Truncate(time.Second)
	svc.now = func() time.Time { return now }
	key := [8]byte{1}
	c, err := svc.GetProofChallenge(ctx, 42, key)
	if err != nil {
		t.Fatal(err)
	}
	public, sig := signChallenge(t, c, 1)
	replace := func(k [8]byte, pub, signature []byte, allow bool) error {
		_, _, err := svc.ReplaceImported(ctx, 42, k, pub, int(c.IssuedAt.Unix()), signature, allow)
		return err
	}
	if err := replace([8]byte{2}, public, sig, false); !errors.Is(err, domain.ErrWalletProofInvalid) {
		t.Fatalf("cross-session: %v", err)
	}
	bad := bytes.Clone(sig)
	bad[0] ^= 1
	if err := replace(key, public, bad, false); !errors.Is(err, domain.ErrWalletProofInvalid) {
		t.Fatalf("bad proof: %v", err)
	}
	provider.err = ErrProviderUnavailable
	if err := replace(key, public, sig, false); !errors.Is(err, ErrProviderUnavailable) {
		t.Fatalf("provider: %v", err)
	}
	links, err := svc.Links(ctx, []int64{42})
	if err != nil || len(links) != 0 {
		t.Fatalf("failed provider persisted link: %v %v", links, err)
	}
	provider.err = nil
	link, balance, err := svc.ReplaceImported(ctx, 42, key, public, int(c.IssuedAt.Unix()), sig, false)
	if err != nil || balance != 1234567890 || link.Address == "" {
		t.Fatalf("link: %+v %d %v", link, balance, err)
	}
	if err := store.Close(); err != nil {
		t.Fatal(err)
	}
	store, err = NewFileStore(dir)
	if err != nil {
		t.Fatal(err)
	}
	svc.store = store
	if err := replace(key, public, sig, false); err != nil {
		t.Fatalf("retry after restart: %v", err)
	}
	links, err = svc.Links(ctx, []int64{42, 42})
	if err != nil || len(links) != 1 || !bytes.Equal(links[0].PublicKey, public) {
		t.Fatalf("persisted links: %v %v", links, err)
	}
	oldSig := sig
	c, err = svc.GetProofChallenge(ctx, 42, key)
	if err != nil {
		t.Fatal(err)
	}
	if err := replace(key, public, oldSig, false); !errors.Is(err, domain.ErrWalletProofInvalid) {
		t.Fatalf("superseded challenge: %v", err)
	}
	public, sig = signChallenge(t, c, 2)
	if err := replace(key, public, sig, false); !errors.Is(err, domain.ErrWalletPasswordRequired) {
		t.Fatalf("replacement without password: %v", err)
	}
	if err := replace(key, public, sig, true); err != nil {
		t.Fatal(err)
	}
	now = c.ExpiresAt
	if err := replace(key, public, sig, true); !errors.Is(err, domain.ErrWalletProofInvalid) {
		t.Fatalf("expiry: %v", err)
	}
	info, err := os.Stat(filepath.Join(dir, "42.json"))
	if err != nil || info.Mode().Perm() != 0600 {
		t.Fatalf("private file: %v %v", info, err)
	}
}

func TestFileWalletConcurrentClaims(t *testing.T) {
	store, err := NewFileStore(filepath.Join(t.TempDir(), "wallet"))
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()
	svc := NewService(store, &balanceProvider{}, "wallet.example")
	ctx := context.Background()
	var challenges [2]domain.WalletChallenge
	for i := range challenges {
		challenges[i], err = svc.GetProofChallenge(ctx, 42, [8]byte{byte(i + 1)})
		if err != nil {
			t.Fatal(err)
		}
	}
	var wg sync.WaitGroup
	results := make(chan error, 2)
	for i, c := range challenges {
		pub, sig := signChallenge(t, c, byte(i+1))
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, _, err := svc.ReplaceImported(ctx, 42, c.AuthKeyID, pub, int(c.IssuedAt.Unix()), sig, false)
			results <- err
		}()
	}
	wg.Wait()
	close(results)
	success, denied := 0, 0
	for err := range results {
		if err == nil {
			success++
		} else if errors.Is(err, domain.ErrWalletPasswordRequired) {
			denied++
		} else {
			t.Fatal(err)
		}
	}
	if success != 1 || denied != 1 {
		t.Fatalf("success=%d denied=%d", success, denied)
	}
}

func TestFileWalletLockAndCorruption(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "wallet")
	store, err := NewFileStore(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()
	second, err := NewFileStore(dir)
	if err == nil {
		second.Close()
		t.Fatal("accepted second writer")
	}
	path := filepath.Join(dir, "42.json")
	bad := []byte(`{"version":1,"link":`)
	if err := os.WriteFile(path, bad, 0600); err != nil {
		t.Fatal(err)
	}
	svc := NewService(store, &balanceProvider{}, "wallet.example")
	if _, err := svc.GetProofChallenge(context.Background(), 42, [8]byte{1}); !errors.Is(err, ErrWalletStore) {
		t.Fatalf("corrupt state: %v", err)
	}
	data, err := os.ReadFile(path)
	if err != nil || !bytes.Equal(data, bad) {
		t.Fatal("corrupt record overwritten")
	}
}