package service import ( "context" "fmt" "testing" "time" "hash-of-wisdom/internal/pow/challenge" "hash-of-wisdom/internal/pow/solver" "hash-of-wisdom/internal/quotes" "hash-of-wisdom/internal/service/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWisdomService_FullWorkflowTests(t *testing.T) { tests := []struct { name string resource string difficulty int setupQuote func(*mocks.MockQuoteService) wantErr bool expectQuote bool }{ { name: "successful flow with difficulty 1", resource: "quotes", difficulty: 1, setupQuote: func(m *mocks.MockQuoteService) { m.EXPECT().GetRandomQuote(context.Background()).Return("es.Quote{ Text: "Success quote", Author: "Test Author", }, nil).Once() }, wantErr: false, expectQuote: true, }, { name: "successful flow with difficulty 2", resource: "quotes", difficulty: 2, setupQuote: func(m *mocks.MockQuoteService) { m.EXPECT().GetRandomQuote(context.Background()).Return("es.Quote{ Text: "Another quote", Author: "Another Author", }, nil).Once() }, wantErr: false, expectQuote: true, }, { name: "successful flow with difficulty 3", resource: "quotes", difficulty: 3, setupQuote: func(m *mocks.MockQuoteService) { m.EXPECT().GetRandomQuote(context.Background()).Return("es.Quote{ Text: "Hard quote", Author: "Hard Author", }, nil).Once() }, wantErr: false, expectQuote: true, }, { name: "invalid resource", resource: "invalid", difficulty: 1, setupQuote: func(m *mocks.MockQuoteService) { // No expectations - should not reach quote service }, wantErr: true, expectQuote: false, }, { name: "empty resource", resource: "", difficulty: 1, setupQuote: func(m *mocks.MockQuoteService) { // No expectations - should not reach quote service }, wantErr: true, expectQuote: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create real PoW components config, err := challenge.NewConfig( challenge.WithDefaultDifficulty(tt.difficulty), challenge.WithMinDifficulty(1), challenge.WithMaxDifficulty(10), ) require.NoError(t, err) generator := challenge.NewGenerator(config) verifier := challenge.NewVerifier(config) powSolver := solver.NewSolver(solver.WithWorkers(2)) // Mock quote service mockQuote := mocks.NewMockQuoteService(t) tt.setupQuote(mockQuote) // Create service service := NewWisdomService(NewGeneratorAdapter(generator), verifier, mockQuote) ctx := context.Background() // Step 1: Generate challenge ch, err := service.GenerateChallenge(ctx, tt.resource) if tt.wantErr && (tt.resource == "" || tt.resource != "quotes") { assert.Error(t, err) return } require.NoError(t, err) require.NotNil(t, ch) // Challenge generated successfully (HMAC ensures integrity) // Step 2: Solve the challenge solveCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() solution, err := powSolver.Solve(solveCtx, ch) require.NoError(t, err) require.NotNil(t, solution) // Step 3: Verify solution through service err = service.VerifySolution(ctx, solution) require.NoError(t, err) // Step 4: Get quote if tt.expectQuote { quote, err := service.GetQuote(ctx) require.NoError(t, err) require.NotNil(t, quote) assert.NotEmpty(t, quote.Text) assert.NotEmpty(t, quote.Author) } }) } } func TestWisdomService_FullWorkflow_MultipleRounds(t *testing.T) { // Test multiple consecutive challenge-solve-quote cycles config, err := challenge.NewConfig( challenge.WithDefaultDifficulty(2), challenge.WithMinDifficulty(1), challenge.WithMaxDifficulty(5), ) require.NoError(t, err) generator := challenge.NewGenerator(config) verifier := challenge.NewVerifier(config) powSolver := solver.NewSolver(solver.WithWorkers(1)) mockQuote := mocks.NewMockQuoteService(t) // Setup expectations for 3 rounds quotes := []*quotes.Quote{ {Text: "First quote", Author: "Author 1"}, {Text: "Second quote", Author: "Author 2"}, {Text: "Third quote", Author: "Author 3"}, } for _, quote := range quotes { mockQuote.EXPECT().GetRandomQuote(context.Background()).Return(quote, nil).Once() } service := NewWisdomService(NewGeneratorAdapter(generator), verifier, mockQuote) ctx := context.Background() // Run 3 complete cycles for i, expectedQuote := range quotes { t.Run(fmt.Sprintf("round_%d", i+1), func(t *testing.T) { // Generate challenge ch, err := service.GenerateChallenge(ctx, "quotes") require.NoError(t, err) // Solve challenge solveCtx, cancel := context.WithTimeout(ctx, 5*time.Second) solution, err := powSolver.Solve(solveCtx, ch) cancel() require.NoError(t, err) // Verify solution err = service.VerifySolution(ctx, solution) require.NoError(t, err) // Get quote quote, err := service.GetQuote(ctx) require.NoError(t, err) assert.Equal(t, expectedQuote.Text, quote.Text) assert.Equal(t, expectedQuote.Author, quote.Author) }) } } func TestWisdomService_InvalidSolutions(t *testing.T) { config, err := challenge.NewConfig( challenge.WithDefaultDifficulty(3), challenge.WithMinDifficulty(1), challenge.WithMaxDifficulty(10), ) require.NoError(t, err) generator := challenge.NewGenerator(config) verifier := challenge.NewVerifier(config) powSolver := solver.NewSolver(solver.WithWorkers(1)) mockQuote := mocks.NewMockQuoteService(t) service := NewWisdomService(NewGeneratorAdapter(generator), verifier, mockQuote) ctx := context.Background() // Generate a valid challenge ch, err := service.GenerateChallenge(ctx, "quotes") require.NoError(t, err) // Solve it properly solveCtx, cancel := context.WithTimeout(ctx, 5*time.Second) validSolution, err := powSolver.Solve(solveCtx, ch) cancel() require.NoError(t, err) tests := []struct { name string solution *challenge.Solution wantErr error }{ { name: "nil solution", solution: nil, wantErr: ErrSolutionRequired, }, { name: "tampered challenge", solution: &challenge.Solution{ Challenge: challenge.Challenge{ Timestamp: ch.Timestamp, Difficulty: ch.Difficulty + 1, // Tampered Resource: ch.Resource, Random: ch.Random, HMAC: ch.HMAC, // Original HMAC won't match tampered data }, Nonce: validSolution.Nonce, }, wantErr: ErrInvalidChallenge, }, { name: "wrong nonce", solution: &challenge.Solution{ Challenge: *ch, Nonce: validSolution.Nonce + 1, // Wrong nonce }, wantErr: ErrInvalidSolution, }, { name: "expired challenge", solution: func() *challenge.Solution { expiredCh := &challenge.Challenge{ Timestamp: time.Now().Add(-10 * time.Minute).Unix(), // Expired Difficulty: ch.Difficulty, Resource: ch.Resource, Random: ch.Random, } expiredCh.Sign(config.HMACSecret) return &challenge.Solution{ Challenge: *expiredCh, Nonce: 0, // Difficulty doesn't matter since it will fail on expiry } }(), wantErr: ErrInvalidChallenge, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := service.VerifySolution(ctx, tt.solution) assert.ErrorIs(t, err, tt.wantErr) }) } } func TestWisdomService_UnsuccessfulFlows(t *testing.T) { tests := []struct { name string difficulty int createSolution func(*challenge.Challenge, *challenge.Solution) *challenge.Solution }{ { name: "tampered solution", difficulty: 2, createSolution: func(ch *challenge.Challenge, validSolution *challenge.Solution) *challenge.Solution { return &challenge.Solution{ Challenge: challenge.Challenge{ Timestamp: ch.Timestamp, Difficulty: ch.Difficulty + 1, // Tampered Resource: ch.Resource, Random: ch.Random, HMAC: ch.HMAC, // Original HMAC won't match tampered data }, Nonce: validSolution.Nonce, } }, }, { name: "wrong nonce", difficulty: 3, createSolution: func(ch *challenge.Challenge, validSolution *challenge.Solution) *challenge.Solution { return &challenge.Solution{ Challenge: *ch, Nonce: 999999, // Wrong nonce } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create real PoW components config, err := challenge.NewConfig( challenge.WithDefaultDifficulty(tt.difficulty), challenge.WithMinDifficulty(1), challenge.WithMaxDifficulty(10), ) require.NoError(t, err) generator := challenge.NewGenerator(config) verifier := challenge.NewVerifier(config) powSolver := solver.NewSolver(solver.WithWorkers(2)) // Mock quote service - should not be called mockQuote := mocks.NewMockQuoteService(t) // No expectations - service should not reach quote fetching // Create service service := NewWisdomService(NewGeneratorAdapter(generator), verifier, mockQuote) ctx := context.Background() // Step 1: Generate challenge ch, err := service.GenerateChallenge(ctx, "quotes") require.NoError(t, err) require.NotNil(t, ch) // Step 2: Get a valid solution first (for tampering tests) solveCtx, cancel := context.WithTimeout(ctx, 5*time.Second) validSolution, err := powSolver.Solve(solveCtx, ch) cancel() require.NoError(t, err) // Step 3: Create invalid solution invalidSolution := tt.createSolution(ch, validSolution) // Step 4: Verify solution should fail err = service.VerifySolution(ctx, invalidSolution) require.Error(t, err) // Verify error type based on test case if tt.name == "tampered solution" { require.ErrorIs(t, err, ErrInvalidChallenge) } else if tt.name == "wrong nonce" { require.ErrorIs(t, err, ErrInvalidSolution) } // Quote service should never be called in unsuccessful flows }) } }