package solver import ( "context" "testing" "time" "hash-of-wisdom/internal/pow/challenge" ) func TestNewSolver(t *testing.T) { solver := NewSolver(WithWorkers(4)) if solver == nil { t.Fatal("expected solver to be created") } if solver.workers != 4 { t.Errorf("expected 4 workers, got %d", solver.workers) } } func TestSolver_SolveEasyChallenge(t *testing.T) { solver := NewSolver(WithWorkers(2)) // Use fewer workers for simpler test // Create a challenge with low difficulty ch := &challenge.Challenge{ Timestamp: time.Now().Unix(), Difficulty: 1, // Very low difficulty should solve quickly Resource: "quotes", Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() solution, err := solver.Solve(ctx, ch) if err != nil { t.Fatalf("expected solution to be found, got error: %v", err) } if solution == nil { t.Fatal("expected solution to be returned") } // Verify the solution is valid if !solution.Verify() { t.Error("expected solution to be valid") } // Verify solution contains original challenge if solution.Challenge.Difficulty != ch.Difficulty { t.Errorf("expected challenge difficulty %d, got %d", ch.Difficulty, solution.Challenge.Difficulty) } if string(solution.Challenge.Random) != string(ch.Random) { t.Errorf("expected challenge random %x, got %x", ch.Random, solution.Challenge.Random) } } func TestSolver_SolveWithCancellation(t *testing.T) { solver := NewSolver(WithWorkers(2)) // Create a challenge with very high difficulty (extremely unlikely to solve quickly) ch := &challenge.Challenge{ Timestamp: time.Now().Unix(), Difficulty: 25, // Extremely high difficulty - will require billions of attempts Resource: "quotes", Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, } ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() start := time.Now() solution, err := solver.Solve(ctx, ch) elapsed := time.Since(start) // Should timeout and return context error (unless we get extremely lucky) if err == nil { t.Log("Got extremely lucky and solved difficulty 25 quickly - this is statistically very unlikely!") if !solution.Verify() { t.Error("solution should be valid if we got one") } return } if err != context.DeadlineExceeded { t.Fatalf("expected context.DeadlineExceeded, got: %v", err) } if solution != nil { t.Error("expected no solution when context cancelled") } // Should return quickly due to cancellation (within reasonable time) if elapsed > 200*time.Millisecond { t.Errorf("solver took too long to cancel: %v", elapsed) } } func TestSolver_SolveDifficulty0(t *testing.T) { solver := NewSolver(WithWorkers(1)) // Difficulty 0 should always pass (no leading zero bits required) ch := &challenge.Challenge{ Timestamp: time.Now().Unix(), Difficulty: 0, Resource: "quotes", Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() solution, err := solver.Solve(ctx, ch) if err != nil { t.Fatalf("expected solution for difficulty 0, got error: %v", err) } if !solution.Verify() { t.Error("expected solution to be valid for difficulty 0") } // Should find solution almost immediately (nonce 0 should work) if solution.Nonce != 0 { t.Log("Note: nonce 0 didn't work, but that's okay - any nonce works for difficulty 0") } } func TestSolver_MultipleWorkers(t *testing.T) { // Test that multiple workers work correctly tests := []struct { name string workers int }{ {"single worker", 1}, {"multiple workers", 4}, {"many workers", 8}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { solver := NewSolver(WithWorkers(tt.workers)) ch := &challenge.Challenge{ Timestamp: time.Now().Unix(), Difficulty: 2, // Moderate difficulty Resource: "quotes", Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() solution, err := solver.Solve(ctx, ch) if err != nil { t.Fatalf("expected solution with %d workers, got error: %v", tt.workers, err) } if !solution.Verify() { t.Errorf("expected valid solution with %d workers", tt.workers) } }) } } func TestSolver_ConcurrentSolves(t *testing.T) { solver := NewSolver(WithWorkers(4)) // Test that solver can handle multiple concurrent solve requests const numConcurrent = 3 results := make(chan error, numConcurrent) for i := 0; i < numConcurrent; i++ { go func(id int) { ch := &challenge.Challenge{ Timestamp: time.Now().Unix(), Difficulty: 2, Resource: "quotes", Random: []byte{byte(id), 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, // Different random for each } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() solution, err := solver.Solve(ctx, ch) if err != nil { results <- err return } if !solution.Verify() { results <- err return } results <- nil }(i) } // Wait for all goroutines to complete for i := 0; i < numConcurrent; i++ { select { case err := <-results: if err != nil { t.Fatalf("concurrent solve %d failed: %v", i, err) } case <-time.After(10 * time.Second): t.Fatal("concurrent solve timed out") } } } func BenchmarkSolver_Solve(b *testing.B) { solver := NewSolver(WithWorkers(4)) ch := &challenge.Challenge{ Timestamp: time.Now().Unix(), Difficulty: 3, // Moderate difficulty for benchmarking Resource: "quotes", Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, } ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { // Use different random for each iteration to avoid caching ch.Random[0] = byte(i) _, err := solver.Solve(ctx, ch) if err != nil { b.Fatalf("benchmark solve failed: %v", err) } } } func TestFullRoundtrip(t *testing.T) { // Test full roundtrip: generate challenge -> solve -> verify config := challenge.TestConfig() generator := challenge.NewGenerator(config) verifier := challenge.NewVerifier(config) solver := NewSolver(WithWorkers(2)) // Generate a challenge ch, err := generator.GenerateChallenge() if err != nil { t.Fatalf("failed to generate challenge: %v", err) } // Verify the challenge is valid err = verifier.VerifyChallenge(ch) if err != nil { t.Fatalf("generated challenge failed verification: %v", err) } // Set low difficulty for quick solve ch.Difficulty = 2 ch.Sign(config.HMACSecret) // Re-sign after modifying difficulty // Solve the challenge ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() solution, err := solver.Solve(ctx, ch) if err != nil { t.Fatalf("failed to solve challenge: %v", err) } // SERVER-SIDE VERIFICATION: Behave exactly like the server would // 1. First verify the challenge authenticity and expiration (like server does) err = verifier.VerifyChallenge(&solution.Challenge) if err != nil { t.Fatalf("server challenge verification failed: %v", err) } // 2. Then verify the PoW solution (like server does) if !solution.Verify() { t.Fatal("server PoW solution verification failed") } // Success - server would now grant access to the quote! }