diff --git a/.mockery.yaml b/.mockery.yaml index 877ad49..6105c3d 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -9,3 +9,6 @@ packages: QuoteService: ChallengeGenerator: ChallengeVerifier: + hash-of-wisdom/internal/controller: + interfaces: + WisdomService: diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go new file mode 100644 index 0000000..d9861ce --- /dev/null +++ b/internal/controller/controller_test.go @@ -0,0 +1,314 @@ +package controller + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "hash-of-wisdom/internal/controller/mocks" + "hash-of-wisdom/internal/pow/challenge" + "hash-of-wisdom/internal/protocol" + "hash-of-wisdom/internal/quotes" + "hash-of-wisdom/internal/service" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNewWisdomController(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + assert.NotNil(t, controller) + assert.Equal(t, mockService, controller.wisdomService) + assert.NotNil(t, controller.codec) +} + +func TestWisdomController_HandleMessage_UnsupportedType(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.MessageType(0xFF), // Invalid type + Payload: []byte{}, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + + var errorPayload protocol.ErrorResponsePayload + err = json.Unmarshal(response.Payload, &errorPayload) + require.NoError(t, err) + assert.Equal(t, protocol.ErrMalformedMessage, errorPayload.Code) + assert.Contains(t, errorPayload.Message, "unsupported message type: 0xff") +} + +func TestWisdomController_HandleChallengeRequest_Success(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // Mock successful challenge generation + testChallenge := &challenge.Challenge{ + Resource: "quotes", + Timestamp: 12345, + Difficulty: 4, + Random: []byte("test"), + HMAC: []byte("signature"), + } + + mockService.On("GenerateChallenge", mock.Anything, "quotes").Return(testChallenge, nil) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.ChallengeRequest, + Payload: []byte{}, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ChallengeResponse, response.Type) + + // Verify the challenge is properly marshaled + var challengePayload challenge.Challenge + err = json.Unmarshal(response.Payload, &challengePayload) + require.NoError(t, err) + assert.Equal(t, testChallenge.Resource, challengePayload.Resource) + assert.Equal(t, testChallenge.Difficulty, challengePayload.Difficulty) + + mockService.AssertExpectations(t) +} + +func TestWisdomController_HandleChallengeRequest_ServiceError(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // Mock service error + mockService.On("GenerateChallenge", mock.Anything, "quotes").Return(nil, errors.New("service error")) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.ChallengeRequest, + Payload: []byte{}, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + + var errorPayload protocol.ErrorResponsePayload + err = json.Unmarshal(response.Payload, &errorPayload) + require.NoError(t, err) + assert.Equal(t, protocol.ErrServerError, errorPayload.Code) + assert.Equal(t, "failed to generate challenge", errorPayload.Message) + + mockService.AssertExpectations(t) +} + +func TestWisdomController_HandleSolutionRequest_Success(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // Create test solution request + testChallenge := challenge.Challenge{ + Resource: "quotes", + Timestamp: 12345, + Difficulty: 4, + Random: []byte("test"), + HMAC: []byte("signature"), + } + + solutionPayload := protocol.SolutionRequestPayload{ + Challenge: testChallenge, + Nonce: 12345, + } + + payloadJSON, err := json.Marshal(solutionPayload) + require.NoError(t, err) + + testQuote := "es.Quote{ + Text: "Test quote", + Author: "Test Author", + } + + // Mock successful verification and quote retrieval + mockService.On("VerifySolution", mock.Anything, mock.AnythingOfType("*challenge.Solution")).Return(nil) + mockService.On("GetQuote", mock.Anything).Return(testQuote, nil) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.SolutionRequest, + Payload: payloadJSON, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.QuoteResponse, response.Type) + + // Verify the quote is properly marshaled + var quotePayload quotes.Quote + err = json.Unmarshal(response.Payload, "ePayload) + require.NoError(t, err) + assert.Equal(t, testQuote.Text, quotePayload.Text) + assert.Equal(t, testQuote.Author, quotePayload.Author) + + mockService.AssertExpectations(t) +} + +func TestWisdomController_HandleSolutionRequest_InvalidJSON(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.SolutionRequest, + Payload: []byte("invalid json"), + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + + var errorPayload protocol.ErrorResponsePayload + err = json.Unmarshal(response.Payload, &errorPayload) + require.NoError(t, err) + assert.Equal(t, protocol.ErrMalformedMessage, errorPayload.Code) + assert.Equal(t, "invalid solution format", errorPayload.Message) + + mockService.AssertNotCalled(t, "VerifySolution") + mockService.AssertNotCalled(t, "GetQuote") +} + +func TestWisdomController_HandleSolutionRequest_VerificationFailed(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // Create test solution request + testChallenge := challenge.Challenge{ + Resource: "quotes", + Timestamp: 12345, + Difficulty: 4, + Random: []byte("test"), + HMAC: []byte("signature"), + } + + solutionPayload := protocol.SolutionRequestPayload{ + Challenge: testChallenge, + Nonce: 12345, + } + + payloadJSON, err := json.Marshal(solutionPayload) + require.NoError(t, err) + + // Mock verification failure + mockService.On("VerifySolution", mock.Anything, mock.AnythingOfType("*challenge.Solution")).Return(service.ErrInvalidSolution) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.SolutionRequest, + Payload: payloadJSON, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + + var errorPayload protocol.ErrorResponsePayload + err = json.Unmarshal(response.Payload, &errorPayload) + require.NoError(t, err) + assert.Equal(t, protocol.ErrInvalidSolution, errorPayload.Code) + assert.Equal(t, "solution verification failed", errorPayload.Message) + + mockService.AssertExpectations(t) + mockService.AssertNotCalled(t, "GetQuote") +} + +func TestWisdomController_HandleSolutionRequest_QuoteServiceError(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // Create test solution request + testChallenge := challenge.Challenge{ + Resource: "quotes", + Timestamp: 12345, + Difficulty: 4, + Random: []byte("test"), + HMAC: []byte("signature"), + } + + solutionPayload := protocol.SolutionRequestPayload{ + Challenge: testChallenge, + Nonce: 12345, + } + + payloadJSON, err := json.Marshal(solutionPayload) + require.NoError(t, err) + + // Mock successful verification but quote service error + mockService.On("VerifySolution", mock.Anything, mock.AnythingOfType("*challenge.Solution")).Return(nil) + mockService.On("GetQuote", mock.Anything).Return(nil, errors.New("quote service error")) + + ctx := context.Background() + msg := &protocol.Message{ + Type: protocol.SolutionRequest, + Payload: payloadJSON, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + + var errorPayload protocol.ErrorResponsePayload + err = json.Unmarshal(response.Payload, &errorPayload) + require.NoError(t, err) + assert.Equal(t, protocol.ErrServerError, errorPayload.Code) + assert.Equal(t, "failed to get quote", errorPayload.Message) + + mockService.AssertExpectations(t) +} + +func TestWisdomController_CreateErrorResponse_MarshalError(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // This test verifies the internal error handling + // In practice, json.Marshal on ErrorResponsePayload should never fail + // But we test the error path for completeness + response, err := controller.createErrorResponse(protocol.ErrServerError, "test message") + + // Should not fail with normal inputs + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + assert.NotEmpty(t, response.Payload) +} + +func TestWisdomController_HandleMessage_ContextCancellation(t *testing.T) { + mockService := mocks.NewMockWisdomService(t) + controller := NewWisdomController(mockService) + + // Create cancelled context + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // Mock service to respect context cancellation + mockService.On("GenerateChallenge", mock.Anything, "quotes").Return(nil, context.Canceled) + + msg := &protocol.Message{ + Type: protocol.ChallengeRequest, + Payload: []byte{}, + } + + response, err := controller.HandleMessage(ctx, msg) + require.NoError(t, err) + assert.Equal(t, protocol.ErrorResponse, response.Type) + + var errorPayload protocol.ErrorResponsePayload + err = json.Unmarshal(response.Payload, &errorPayload) + require.NoError(t, err) + assert.Equal(t, protocol.ErrServerError, errorPayload.Code) + + mockService.AssertExpectations(t) +} diff --git a/internal/controller/mocks/mock_wisdom_service.go b/internal/controller/mocks/mock_wisdom_service.go new file mode 100644 index 0000000..eb039bb --- /dev/null +++ b/internal/controller/mocks/mock_wisdom_service.go @@ -0,0 +1,203 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + context "context" + challenge "hash-of-wisdom/internal/pow/challenge" + + mock "github.com/stretchr/testify/mock" + + quotes "hash-of-wisdom/internal/quotes" +) + +// MockWisdomService is an autogenerated mock type for the WisdomService type +type MockWisdomService struct { + mock.Mock +} + +type MockWisdomService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockWisdomService) EXPECT() *MockWisdomService_Expecter { + return &MockWisdomService_Expecter{mock: &_m.Mock} +} + +// GenerateChallenge provides a mock function with given fields: ctx, resource +func (_m *MockWisdomService) GenerateChallenge(ctx context.Context, resource string) (*challenge.Challenge, error) { + ret := _m.Called(ctx, resource) + + 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(context.Context, string) (*challenge.Challenge, error)); ok { + return rf(ctx, resource) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *challenge.Challenge); ok { + r0 = rf(ctx, resource) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*challenge.Challenge) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, resource) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockWisdomService_GenerateChallenge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateChallenge' +type MockWisdomService_GenerateChallenge_Call struct { + *mock.Call +} + +// GenerateChallenge is a helper method to define mock.On call +// - ctx context.Context +// - resource string +func (_e *MockWisdomService_Expecter) GenerateChallenge(ctx interface{}, resource interface{}) *MockWisdomService_GenerateChallenge_Call { + return &MockWisdomService_GenerateChallenge_Call{Call: _e.mock.On("GenerateChallenge", ctx, resource)} +} + +func (_c *MockWisdomService_GenerateChallenge_Call) Run(run func(ctx context.Context, resource string)) *MockWisdomService_GenerateChallenge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockWisdomService_GenerateChallenge_Call) Return(_a0 *challenge.Challenge, _a1 error) *MockWisdomService_GenerateChallenge_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockWisdomService_GenerateChallenge_Call) RunAndReturn(run func(context.Context, string) (*challenge.Challenge, error)) *MockWisdomService_GenerateChallenge_Call { + _c.Call.Return(run) + return _c +} + +// GetQuote provides a mock function with given fields: ctx +func (_m *MockWisdomService) GetQuote(ctx context.Context) (*quotes.Quote, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetQuote") + } + + 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 +} + +// MockWisdomService_GetQuote_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuote' +type MockWisdomService_GetQuote_Call struct { + *mock.Call +} + +// GetQuote is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockWisdomService_Expecter) GetQuote(ctx interface{}) *MockWisdomService_GetQuote_Call { + return &MockWisdomService_GetQuote_Call{Call: _e.mock.On("GetQuote", ctx)} +} + +func (_c *MockWisdomService_GetQuote_Call) Run(run func(ctx context.Context)) *MockWisdomService_GetQuote_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockWisdomService_GetQuote_Call) Return(_a0 *quotes.Quote, _a1 error) *MockWisdomService_GetQuote_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockWisdomService_GetQuote_Call) RunAndReturn(run func(context.Context) (*quotes.Quote, error)) *MockWisdomService_GetQuote_Call { + _c.Call.Return(run) + return _c +} + +// VerifySolution provides a mock function with given fields: ctx, solution +func (_m *MockWisdomService) VerifySolution(ctx context.Context, solution *challenge.Solution) error { + ret := _m.Called(ctx, solution) + + if len(ret) == 0 { + panic("no return value specified for VerifySolution") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *challenge.Solution) error); ok { + r0 = rf(ctx, solution) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockWisdomService_VerifySolution_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifySolution' +type MockWisdomService_VerifySolution_Call struct { + *mock.Call +} + +// VerifySolution is a helper method to define mock.On call +// - ctx context.Context +// - solution *challenge.Solution +func (_e *MockWisdomService_Expecter) VerifySolution(ctx interface{}, solution interface{}) *MockWisdomService_VerifySolution_Call { + return &MockWisdomService_VerifySolution_Call{Call: _e.mock.On("VerifySolution", ctx, solution)} +} + +func (_c *MockWisdomService_VerifySolution_Call) Run(run func(ctx context.Context, solution *challenge.Solution)) *MockWisdomService_VerifySolution_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*challenge.Solution)) + }) + return _c +} + +func (_c *MockWisdomService_VerifySolution_Call) Return(_a0 error) *MockWisdomService_VerifySolution_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWisdomService_VerifySolution_Call) RunAndReturn(run func(context.Context, *challenge.Solution) error) *MockWisdomService_VerifySolution_Call { + _c.Call.Return(run) + return _c +} + +// NewMockWisdomService creates a new instance of MockWisdomService. 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 NewMockWisdomService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWisdomService { + mock := &MockWisdomService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}