371 lines
9.9 KiB
Go
371 lines
9.9 KiB
Go
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
|
|
})
|
|
}
|
|
}
|