Implement service layer #3
11
.mockery.yaml
Normal file
11
.mockery.yaml
Normal 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:
|
||||||
|
|
@ -74,7 +74,7 @@ tasks:
|
||||||
mocks:
|
mocks:
|
||||||
desc: Generate mocks using mockery
|
desc: Generate mocks using mockery
|
||||||
cmds:
|
cmds:
|
||||||
- mockery
|
- go run github.com/vektra/mockery/v2@latest
|
||||||
|
|
||||||
check:
|
check:
|
||||||
desc: Run all checks (fmt, build, test, lint)
|
desc: Run all checks (fmt, build, test, lint)
|
||||||
|
|
|
||||||
|
|
@ -46,21 +46,25 @@
|
||||||
- [X] Implement quote fetching with HTTP client
|
- [X] Implement quote fetching with HTTP client
|
||||||
- [X] Add basic error handling
|
- [X] Add basic error handling
|
||||||
|
|
||||||
## Phase 3: Basic Server Architecture
|
## Phase 3: Service Layer Implementation
|
||||||
- [ ] Set up dependency injection framework (wire/dig)
|
**Goal**: Complete service layer with DI for handling requests (untied from TCP presentation)
|
||||||
- [ ] Create core interfaces and contracts
|
|
||||||
|
- [X] Create service layer interfaces and contracts
|
||||||
|
- [X] Implement quote request service workflow
|
||||||
|
- [X] Integrate PoW challenge generation and verification
|
||||||
|
- [X] Set up simple dependency wiring
|
||||||
|
- [X] Implement full request-response cycle
|
||||||
|
- [X] Add comprehensive service layer tests
|
||||||
|
- [X] Add error handling and validation
|
||||||
|
- [X] Create public concrete WisdomService type
|
||||||
|
- [X] Add workflow tests with real PoW implementations
|
||||||
|
- [X] Add unsuccessful flow tests for invalid solutions
|
||||||
|
|
||||||
|
## Phase 4: Basic Server Architecture
|
||||||
- [ ] Set up structured logging (zerolog/logrus)
|
- [ ] Set up structured logging (zerolog/logrus)
|
||||||
- [ ] Set up metrics collection (prometheus)
|
- [ ] Set up metrics collection (prometheus)
|
||||||
- [ ] Create configuration management
|
- [ ] Create configuration management
|
||||||
- [ ] Integrate PoW and quote packages into server architecture
|
- [ ] Integrate all components into server architecture
|
||||||
|
|
||||||
## Phase 4: Quote Management System
|
|
||||||
- [ ] Define quote storage interface
|
|
||||||
- [ ] Implement in-memory quote repository (fake)
|
|
||||||
- [ ] Create quote selection service (random)
|
|
||||||
- [ ] Load initial quote collection from file/config
|
|
||||||
- [ ] Add quote validation and sanitization
|
|
||||||
- [ ] Write unit tests for quote management
|
|
||||||
|
|
||||||
## Phase 5: TCP Protocol Implementation
|
## Phase 5: TCP Protocol Implementation
|
||||||
- [ ] Implement binary message protocol codec
|
- [ ] Implement binary message protocol codec
|
||||||
|
|
|
||||||
17
go.mod
17
go.mod
|
|
@ -3,7 +3,18 @@ module hash-of-wisdom
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
github.com/go-resty/resty/v2 v2.16.5
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/net v0.33.0 // indirect
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/time v0.8.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
24
go.sum
24
go.sum
|
|
@ -1,6 +1,26 @@
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
6
internal/quotes/quote.go
Normal file
6
internal/quotes/quote.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package quotes
|
||||||
|
|
||||||
|
type Quote struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package quotes
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type Quote struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service interface {
|
|
||||||
GetRandomQuote(ctx context.Context) (*Quote, error)
|
|
||||||
}
|
|
||||||
16
internal/service/adapters.go
Normal file
16
internal/service/adapters.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import "hash-of-wisdom/internal/pow/challenge"
|
||||||
|
|
||||||
|
// generatorAdapter adapts the real challenge.Generator to our interface
|
||||||
|
type generatorAdapter struct {
|
||||||
|
generator *challenge.Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeneratorAdapter(generator *challenge.Generator) ChallengeGenerator {
|
||||||
|
return &generatorAdapter{generator: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *generatorAdapter) GenerateChallenge() (*challenge.Challenge, error) {
|
||||||
|
return a.generator.GenerateChallenge()
|
||||||
|
}
|
||||||
93
internal/service/mocks/mock_challenge_generator.go
Normal file
93
internal/service/mocks/mock_challenge_generator.go
Normal 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
|
||||||
|
}
|
||||||
82
internal/service/mocks/mock_challenge_verifier.go
Normal file
82
internal/service/mocks/mock_challenge_verifier.go
Normal 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
|
||||||
|
}
|
||||||
95
internal/service/mocks/mock_quote_service.go
Normal file
95
internal/service/mocks/mock_quote_service.go
Normal 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
|
||||||
|
}
|
||||||
86
internal/service/wisdom.go
Normal file
86
internal/service/wisdom.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash-of-wisdom/internal/pow/challenge"
|
||||||
|
"hash-of-wisdom/internal/quotes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrResourceRequired = errors.New("resource is required")
|
||||||
|
ErrUnsupportedResource = errors.New("unsupported resource")
|
||||||
|
ErrSolutionRequired = errors.New("solution is required")
|
||||||
|
ErrInvalidChallenge = errors.New("invalid challenge")
|
||||||
|
ErrInvalidSolution = errors.New("invalid proof of work solution")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChallengeGenerator interface {
|
||||||
|
GenerateChallenge() (*challenge.Challenge, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChallengeVerifier interface {
|
||||||
|
VerifyChallenge(ch *challenge.Challenge) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuoteService interface {
|
||||||
|
GetRandomQuote(ctx context.Context) (*quotes.Quote, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WisdomService struct {
|
||||||
|
challengeGenerator ChallengeGenerator
|
||||||
|
challengeVerifier ChallengeVerifier
|
||||||
|
quoteService QuoteService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWisdomService(
|
||||||
|
generator ChallengeGenerator,
|
||||||
|
verifier ChallengeVerifier,
|
||||||
|
quoteService QuoteService,
|
||||||
|
) *WisdomService {
|
||||||
|
return &WisdomService{
|
||||||
|
challengeGenerator: generator,
|
||||||
|
challengeVerifier: verifier,
|
||||||
|
quoteService: quoteService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WisdomService) GenerateChallenge(ctx context.Context, resource string) (*challenge.Challenge, error) {
|
||||||
|
if resource == "" {
|
||||||
|
return nil, ErrResourceRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
if resource != "quotes" {
|
||||||
|
return nil, ErrUnsupportedResource
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := s.challengeGenerator.GenerateChallenge()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WisdomService) VerifySolution(ctx context.Context, solution *challenge.Solution) error {
|
||||||
|
if solution == nil {
|
||||||
|
return ErrSolutionRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify challenge authenticity and expiration
|
||||||
|
if err := s.challengeVerifier.VerifyChallenge(&solution.Challenge); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrInvalidChallenge, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PoW solution
|
||||||
|
if !solution.Verify() {
|
||||||
|
return ErrInvalidSolution
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WisdomService) GetQuote(ctx context.Context) (*quotes.Quote, error) {
|
||||||
|
return s.quoteService.GetRandomQuote(ctx)
|
||||||
|
}
|
||||||
186
internal/service/wisdom_test.go
Normal file
186
internal/service/wisdom_test.go
Normal 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 := "es.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
|
||||||
|
}
|
||||||
|
}
|
||||||
370
internal/service/workflow_test.go
Normal file
370
internal/service/workflow_test.go
Normal 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("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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue