Implement tests

This commit is contained in:
Savely Krendelhoff 2025-08-22 20:38:27 +07:00
parent f38aa214d2
commit 25be919fe5
No known key found for this signature in database
GPG key ID: F70DFD34F40238DE
7 changed files with 838 additions and 1 deletions

11
.mockery.yaml Normal file
View file

@ -0,0 +1,11 @@
with-expecter: true
dir: "{{.InterfaceDir}}/mocks"
filename: "mock_{{.InterfaceName | snakecase}}.go"
mockname: "Mock{{.InterfaceName}}"
outpkg: "mocks"
packages:
hash-of-wisdom/internal/service:
interfaces:
QuoteService:
ChallengeGenerator:
ChallengeVerifier:

View file

@ -74,7 +74,7 @@ tasks:
mocks:
desc: Generate mocks using mockery
cmds:
- mockery
- go run github.com/vektra/mockery/v2@latest
check:
desc: Run all checks (fmt, build, test, lint)

View file

@ -0,0 +1,93 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
challenge "hash-of-wisdom/internal/pow/challenge"
mock "github.com/stretchr/testify/mock"
)
// MockChallengeGenerator is an autogenerated mock type for the ChallengeGenerator type
type MockChallengeGenerator struct {
mock.Mock
}
type MockChallengeGenerator_Expecter struct {
mock *mock.Mock
}
func (_m *MockChallengeGenerator) EXPECT() *MockChallengeGenerator_Expecter {
return &MockChallengeGenerator_Expecter{mock: &_m.Mock}
}
// GenerateChallenge provides a mock function with no fields
func (_m *MockChallengeGenerator) GenerateChallenge() (*challenge.Challenge, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GenerateChallenge")
}
var r0 *challenge.Challenge
var r1 error
if rf, ok := ret.Get(0).(func() (*challenge.Challenge, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *challenge.Challenge); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*challenge.Challenge)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockChallengeGenerator_GenerateChallenge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateChallenge'
type MockChallengeGenerator_GenerateChallenge_Call struct {
*mock.Call
}
// GenerateChallenge is a helper method to define mock.On call
func (_e *MockChallengeGenerator_Expecter) GenerateChallenge() *MockChallengeGenerator_GenerateChallenge_Call {
return &MockChallengeGenerator_GenerateChallenge_Call{Call: _e.mock.On("GenerateChallenge")}
}
func (_c *MockChallengeGenerator_GenerateChallenge_Call) Run(run func()) *MockChallengeGenerator_GenerateChallenge_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockChallengeGenerator_GenerateChallenge_Call) Return(_a0 *challenge.Challenge, _a1 error) *MockChallengeGenerator_GenerateChallenge_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockChallengeGenerator_GenerateChallenge_Call) RunAndReturn(run func() (*challenge.Challenge, error)) *MockChallengeGenerator_GenerateChallenge_Call {
_c.Call.Return(run)
return _c
}
// NewMockChallengeGenerator creates a new instance of MockChallengeGenerator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockChallengeGenerator(t interface {
mock.TestingT
Cleanup(func())
}) *MockChallengeGenerator {
mock := &MockChallengeGenerator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,82 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
challenge "hash-of-wisdom/internal/pow/challenge"
mock "github.com/stretchr/testify/mock"
)
// MockChallengeVerifier is an autogenerated mock type for the ChallengeVerifier type
type MockChallengeVerifier struct {
mock.Mock
}
type MockChallengeVerifier_Expecter struct {
mock *mock.Mock
}
func (_m *MockChallengeVerifier) EXPECT() *MockChallengeVerifier_Expecter {
return &MockChallengeVerifier_Expecter{mock: &_m.Mock}
}
// VerifyChallenge provides a mock function with given fields: ch
func (_m *MockChallengeVerifier) VerifyChallenge(ch *challenge.Challenge) error {
ret := _m.Called(ch)
if len(ret) == 0 {
panic("no return value specified for VerifyChallenge")
}
var r0 error
if rf, ok := ret.Get(0).(func(*challenge.Challenge) error); ok {
r0 = rf(ch)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockChallengeVerifier_VerifyChallenge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyChallenge'
type MockChallengeVerifier_VerifyChallenge_Call struct {
*mock.Call
}
// VerifyChallenge is a helper method to define mock.On call
// - ch *challenge.Challenge
func (_e *MockChallengeVerifier_Expecter) VerifyChallenge(ch interface{}) *MockChallengeVerifier_VerifyChallenge_Call {
return &MockChallengeVerifier_VerifyChallenge_Call{Call: _e.mock.On("VerifyChallenge", ch)}
}
func (_c *MockChallengeVerifier_VerifyChallenge_Call) Run(run func(ch *challenge.Challenge)) *MockChallengeVerifier_VerifyChallenge_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*challenge.Challenge))
})
return _c
}
func (_c *MockChallengeVerifier_VerifyChallenge_Call) Return(_a0 error) *MockChallengeVerifier_VerifyChallenge_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockChallengeVerifier_VerifyChallenge_Call) RunAndReturn(run func(*challenge.Challenge) error) *MockChallengeVerifier_VerifyChallenge_Call {
_c.Call.Return(run)
return _c
}
// NewMockChallengeVerifier creates a new instance of MockChallengeVerifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockChallengeVerifier(t interface {
mock.TestingT
Cleanup(func())
}) *MockChallengeVerifier {
mock := &MockChallengeVerifier{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,95 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
context "context"
quotes "hash-of-wisdom/internal/quotes"
mock "github.com/stretchr/testify/mock"
)
// MockQuoteService is an autogenerated mock type for the QuoteService type
type MockQuoteService struct {
mock.Mock
}
type MockQuoteService_Expecter struct {
mock *mock.Mock
}
func (_m *MockQuoteService) EXPECT() *MockQuoteService_Expecter {
return &MockQuoteService_Expecter{mock: &_m.Mock}
}
// GetRandomQuote provides a mock function with given fields: ctx
func (_m *MockQuoteService) GetRandomQuote(ctx context.Context) (*quotes.Quote, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for GetRandomQuote")
}
var r0 *quotes.Quote
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (*quotes.Quote, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) *quotes.Quote); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*quotes.Quote)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockQuoteService_GetRandomQuote_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRandomQuote'
type MockQuoteService_GetRandomQuote_Call struct {
*mock.Call
}
// GetRandomQuote is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockQuoteService_Expecter) GetRandomQuote(ctx interface{}) *MockQuoteService_GetRandomQuote_Call {
return &MockQuoteService_GetRandomQuote_Call{Call: _e.mock.On("GetRandomQuote", ctx)}
}
func (_c *MockQuoteService_GetRandomQuote_Call) Run(run func(ctx context.Context)) *MockQuoteService_GetRandomQuote_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockQuoteService_GetRandomQuote_Call) Return(_a0 *quotes.Quote, _a1 error) *MockQuoteService_GetRandomQuote_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockQuoteService_GetRandomQuote_Call) RunAndReturn(run func(context.Context) (*quotes.Quote, error)) *MockQuoteService_GetRandomQuote_Call {
_c.Call.Return(run)
return _c
}
// NewMockQuoteService creates a new instance of MockQuoteService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockQuoteService(t interface {
mock.TestingT
Cleanup(func())
}) *MockQuoteService {
mock := &MockQuoteService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,186 @@
package service
import (
"context"
"testing"
"time"
"hash-of-wisdom/internal/pow/challenge"
"hash-of-wisdom/internal/quotes"
"hash-of-wisdom/internal/service/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWisdomService_GenerateChallenge(t *testing.T) {
tests := []struct {
name string
resource string
wantErr error
}{
{
name: "valid resource",
resource: "quotes",
wantErr: nil,
},
{
name: "empty resource",
resource: "",
wantErr: ErrResourceRequired,
},
{
name: "unsupported resource",
resource: "invalid",
wantErr: ErrUnsupportedResource,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockGen := mocks.NewMockChallengeGenerator(t)
mockVer := mocks.NewMockChallengeVerifier(t)
mockQuote := mocks.NewMockQuoteService(t)
service := NewWisdomService(mockGen, mockVer, mockQuote)
if tt.wantErr == nil {
expectedChallenge := &challenge.Challenge{
Timestamp: time.Now().Unix(),
Difficulty: 4,
Resource: "quotes",
Random: []byte{0x01, 0x02, 0x03},
}
mockGen.EXPECT().GenerateChallenge().Return(expectedChallenge, nil).Once()
}
ctx := context.Background()
ch, err := service.GenerateChallenge(ctx, tt.resource)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
assert.Nil(t, ch)
} else {
assert.NoError(t, err)
assert.NotNil(t, ch)
}
})
}
}
func TestWisdomService_VerifySolution(t *testing.T) {
tests := []struct {
name string
solution *challenge.Solution
wantErr error
setupMocks func(*mocks.MockChallengeVerifier)
}{
{
name: "nil solution",
solution: nil,
wantErr: ErrSolutionRequired,
setupMocks: func(mv *mocks.MockChallengeVerifier) {},
},
{
name: "invalid challenge",
solution: &challenge.Solution{
Challenge: challenge.Challenge{},
Nonce: 123,
},
wantErr: ErrInvalidChallenge,
setupMocks: func(mv *mocks.MockChallengeVerifier) {
mv.EXPECT().VerifyChallenge(mock.Anything).Return(challenge.ErrInvalidHMAC).Once()
},
},
{
name: "invalid solution",
solution: createInvalidSolution(t),
wantErr: ErrInvalidSolution,
setupMocks: func(mv *mocks.MockChallengeVerifier) {
mv.EXPECT().VerifyChallenge(mock.Anything).Return(nil).Once()
},
},
{
name: "valid solution",
solution: createValidSolution(t),
wantErr: nil,
setupMocks: func(mv *mocks.MockChallengeVerifier) {
mv.EXPECT().VerifyChallenge(mock.Anything).Return(nil).Once()
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockGen := mocks.NewMockChallengeGenerator(t)
mockVer := mocks.NewMockChallengeVerifier(t)
mockQuote := mocks.NewMockQuoteService(t)
tt.setupMocks(mockVer)
service := NewWisdomService(mockGen, mockVer, mockQuote)
ctx := context.Background()
err := service.VerifySolution(ctx, tt.solution)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
func TestWisdomService_GetQuote(t *testing.T) {
mockGen := mocks.NewMockChallengeGenerator(t)
mockVer := mocks.NewMockChallengeVerifier(t)
mockQuote := mocks.NewMockQuoteService(t)
expectedQuote := &quotes.Quote{
Text: "Test quote",
Author: "Test author",
}
mockQuote.EXPECT().GetRandomQuote(mock.Anything).Return(expectedQuote, nil).Once()
service := NewWisdomService(mockGen, mockVer, mockQuote)
ctx := context.Background()
quote, err := service.GetQuote(ctx)
assert.NoError(t, err)
assert.Equal(t, expectedQuote, quote)
}
func createValidSolution(t *testing.T) *challenge.Solution {
config := challenge.TestConfig()
ch := &challenge.Challenge{
Timestamp: time.Now().Unix(),
Difficulty: 0, // Difficulty 0 means any nonce works
Resource: "quotes",
Random: []byte{0x01, 0x02, 0x03},
}
ch.Sign(config.HMACSecret)
return &challenge.Solution{
Challenge: *ch,
Nonce: 0,
}
}
func createInvalidSolution(t *testing.T) *challenge.Solution {
config := challenge.TestConfig()
ch := &challenge.Challenge{
Timestamp: time.Now().Unix(),
Difficulty: 20, // High difficulty, nonce 999 won't work
Resource: "quotes",
Random: []byte{0x01, 0x02, 0x03},
}
ch.Sign(config.HMACSecret)
return &challenge.Solution{
Challenge: *ch,
Nonce: 999, // Wrong nonce for difficulty 20
}
}

View file

@ -0,0 +1,370 @@
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(&quotes.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(&quotes.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(&quotes.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
})
}
}