internal/app/wallet/filestore_test.go
Rama main · réplica de lectura del repositorio original.
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")
}
}