- 보상 지급 실패 시 즉시 재시도(3회 backoff) + DB 기록 + 백그라운드 워커 재시도 - WaitForTx 폴링으로 블록체인 TX 확정 대기, SendTxAndWait 편의 메서드 - chain 트랜잭션 코드 중복 제거 (userTx/operatorTx 헬퍼, 50% 감소) - AppError 기반 에러 응답 포맷 통일 (8개 코드, 전 핸들러 마이그레이션) - TX 에러 분류 + 한국어 사용자 메시지 매핑 (11가지 패턴) - player 서비스 테스트 20개 + chain WaitForTx 테스트 10개 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
272 lines
7.6 KiB
Go
272 lines
7.6 KiB
Go
package chain
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
tocrypto "github.com/tolelom/tolchain/crypto"
|
|
)
|
|
|
|
// testEncKey returns a valid 32-byte AES-256 key for testing.
|
|
func testEncKey() []byte {
|
|
key := make([]byte, 32)
|
|
for i := range key {
|
|
key[i] = byte(i)
|
|
}
|
|
return key
|
|
}
|
|
|
|
// newTestService creates a minimal Service with only the encryption key set.
|
|
// No DB, Redis, or chain client — only suitable for testing pure crypto functions.
|
|
func newTestService() *Service {
|
|
return &Service{
|
|
encKeyBytes: testEncKey(),
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
|
svc := newTestService()
|
|
|
|
// Generate a real ed25519 private key
|
|
privKey, _, err := tocrypto.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key pair: %v", err)
|
|
}
|
|
|
|
// Encrypt
|
|
cipherHex, nonceHex, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey failed: %v", err)
|
|
}
|
|
|
|
if cipherHex == "" || nonceHex == "" {
|
|
t.Fatal("encryptPrivKey returned empty strings")
|
|
}
|
|
|
|
// Verify ciphertext is valid hex
|
|
if _, err := hex.DecodeString(cipherHex); err != nil {
|
|
t.Errorf("cipherHex is not valid hex: %v", err)
|
|
}
|
|
if _, err := hex.DecodeString(nonceHex); err != nil {
|
|
t.Errorf("nonceHex is not valid hex: %v", err)
|
|
}
|
|
|
|
// Decrypt
|
|
decrypted, err := svc.decryptPrivKey(cipherHex, nonceHex)
|
|
if err != nil {
|
|
t.Fatalf("decryptPrivKey failed: %v", err)
|
|
}
|
|
|
|
// Compare
|
|
if hex.EncodeToString(decrypted) != hex.EncodeToString(privKey) {
|
|
t.Error("decrypted key does not match original")
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecrypt_DifferentKeysProduceDifferentCiphertext(t *testing.T) {
|
|
svc := newTestService()
|
|
|
|
privKey1, _, err := tocrypto.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key pair 1: %v", err)
|
|
}
|
|
privKey2, _, err := tocrypto.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key pair 2: %v", err)
|
|
}
|
|
|
|
cipher1, _, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey1))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey 1 failed: %v", err)
|
|
}
|
|
cipher2, _, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey2))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey 2 failed: %v", err)
|
|
}
|
|
|
|
if cipher1 == cipher2 {
|
|
t.Error("different private keys should produce different ciphertexts")
|
|
}
|
|
}
|
|
|
|
func TestEncryptSameKey_DifferentNonces(t *testing.T) {
|
|
svc := newTestService()
|
|
|
|
privKey, _, err := tocrypto.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key pair: %v", err)
|
|
}
|
|
|
|
cipher1, nonce1, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey 1 failed: %v", err)
|
|
}
|
|
cipher2, nonce2, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey 2 failed: %v", err)
|
|
}
|
|
|
|
// Each encryption should use a different random nonce
|
|
if nonce1 == nonce2 {
|
|
t.Error("encrypting the same key twice should use different nonces")
|
|
}
|
|
|
|
// So ciphertext should also differ (AES-GCM is nonce-dependent)
|
|
if cipher1 == cipher2 {
|
|
t.Error("encrypting the same key with different nonces should produce different ciphertexts")
|
|
}
|
|
}
|
|
|
|
func TestDecryptWithWrongKey(t *testing.T) {
|
|
svc := newTestService()
|
|
|
|
privKey, _, err := tocrypto.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key pair: %v", err)
|
|
}
|
|
|
|
cipherHex, nonceHex, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey failed: %v", err)
|
|
}
|
|
|
|
// Create a service with a different encryption key
|
|
wrongKey := make([]byte, 32)
|
|
for i := range wrongKey {
|
|
wrongKey[i] = byte(255 - i)
|
|
}
|
|
wrongSvc := &Service{encKeyBytes: wrongKey}
|
|
|
|
_, err = wrongSvc.decryptPrivKey(cipherHex, nonceHex)
|
|
if err == nil {
|
|
t.Error("decryptPrivKey with wrong key should fail")
|
|
}
|
|
}
|
|
|
|
func TestDecryptWithInvalidHex(t *testing.T) {
|
|
svc := newTestService()
|
|
|
|
_, err := svc.decryptPrivKey("not-hex", "also-not-hex")
|
|
if err == nil {
|
|
t.Error("decryptPrivKey with invalid hex should fail")
|
|
}
|
|
}
|
|
|
|
func TestDecryptWithTamperedCiphertext(t *testing.T) {
|
|
svc := newTestService()
|
|
|
|
privKey, _, err := tocrypto.GenerateKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key pair: %v", err)
|
|
}
|
|
|
|
cipherHex, nonceHex, err := svc.encryptPrivKey(tocrypto.PrivateKey(privKey))
|
|
if err != nil {
|
|
t.Fatalf("encryptPrivKey failed: %v", err)
|
|
}
|
|
|
|
// Tamper with the ciphertext by flipping a byte
|
|
cipherBytes, _ := hex.DecodeString(cipherHex)
|
|
cipherBytes[0] ^= 0xFF
|
|
tamperedHex := hex.EncodeToString(cipherBytes)
|
|
|
|
_, err = svc.decryptPrivKey(tamperedHex, nonceHex)
|
|
if err == nil {
|
|
t.Error("decryptPrivKey with tampered ciphertext should fail")
|
|
}
|
|
}
|
|
|
|
func TestNewService_InvalidEncryptionKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
encKey string
|
|
}{
|
|
{"too short", "aabbccdd"},
|
|
{"not hex", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"},
|
|
{"empty", ""},
|
|
{"odd length", "aabbccddeeff00112233445566778899aabbccddeeff0011223344556677889"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := NewService(nil, nil, "test-chain", "", tt.encKey)
|
|
if err == nil {
|
|
t.Error("NewService should fail with invalid encryption key")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewService_ValidEncryptionKey(t *testing.T) {
|
|
// 64 hex chars = 32 bytes
|
|
validKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
|
|
svc, err := NewService(nil, nil, "test-chain", "", validKey)
|
|
if err != nil {
|
|
t.Fatalf("NewService with valid key should succeed: %v", err)
|
|
}
|
|
if svc == nil {
|
|
t.Fatal("NewService returned nil service")
|
|
}
|
|
if svc.chainID != "test-chain" {
|
|
t.Errorf("chainID = %q, want %q", svc.chainID, "test-chain")
|
|
}
|
|
// No operator key provided, so operatorWallet should be nil
|
|
if svc.operatorWallet != nil {
|
|
t.Error("operatorWallet should be nil when no operator key is provided")
|
|
}
|
|
}
|
|
|
|
func TestEnsureOperator_NilWallet(t *testing.T) {
|
|
svc := newTestService()
|
|
err := svc.ensureOperator()
|
|
if err == nil {
|
|
t.Error("ensureOperator should fail when operatorWallet is nil")
|
|
}
|
|
}
|
|
|
|
func TestResolveUsername_NoResolver(t *testing.T) {
|
|
svc := newTestService()
|
|
_, err := svc.resolveUsername("testuser")
|
|
if err == nil {
|
|
t.Error("resolveUsername should fail when userResolver is nil")
|
|
}
|
|
}
|
|
|
|
func TestClassifyTxError(t *testing.T) {
|
|
tests := []struct {
|
|
chainMsg string
|
|
want string
|
|
}{
|
|
{"insufficient balance: have 0 need 100: insufficient balance", "잔액이 부족합니다"},
|
|
{"only the asset owner can list it: unauthorized", "권한이 없습니다"},
|
|
{"session \"abc\" already exists: already exists", "이미 존재합니다"},
|
|
{"asset \"xyz\" not found: not found", "리소스를 찾을 수 없습니다"},
|
|
{"asset is not tradeable", "거래할 수 없는 아이템입니다"},
|
|
{"asset \"a\" is equipped; unequip it before listing", "장착 중인 아이템입니다"},
|
|
{"asset \"a\" is already listed (listing x): already exists", "이미 마켓에 등록된 아이템입니다"},
|
|
{"listing \"x\" is not active", "활성 상태가 아닌 매물입니다"},
|
|
{"session \"x\" is not open (status=closed)", "진행 중이 아닌 세션입니다"},
|
|
{"invalid nonce: expected 5 got 3: invalid nonce", "트랜잭션 처리 중 오류가 발생했습니다. 다시 시도해주세요"},
|
|
{"some unknown error", "블록체인 트랜잭션이 실패했습니다"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.chainMsg, func(t *testing.T) {
|
|
got := classifyTxError(tt.chainMsg)
|
|
if got != tt.want {
|
|
t.Errorf("classifyTxError(%q) = %q, want %q", tt.chainMsg, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTxError_Error(t *testing.T) {
|
|
err := &TxError{TxID: "abc123", Message: "insufficient balance"}
|
|
got := err.Error()
|
|
want := "transaction abc123 failed: insufficient balance"
|
|
if got != want {
|
|
t.Errorf("TxError.Error() = %q, want %q", got, want)
|
|
}
|
|
}
|