Phase 5: Rework protocol package and implement application layer #5

Merged
krendelhoff merged 7 commits from phase-5-basic-client-server into master 2025-08-23 08:36:45 +03:00
4 changed files with 618 additions and 0 deletions
Showing only changes of commit 94eb94e167 - Show all commits

View file

@ -9,3 +9,6 @@ packages:
QuoteService:
ChallengeGenerator:
ChallengeVerifier:
hash-of-wisdom/internal/controller:
interfaces:
WisdomService:

View file

@ -0,0 +1,88 @@
package application
import (
"context"
"fmt"
"io"
"hash-of-wisdom/internal/pow/challenge"
"hash-of-wisdom/internal/protocol"
"hash-of-wisdom/internal/quotes"
)
// Response represents an encodable response that can write itself to a connection
type Response interface {
Encode(w io.Writer) error
}
// WisdomService defines the interface for the wisdom service
type WisdomService interface {
GenerateChallenge(ctx context.Context, resource string) (*challenge.Challenge, error)
VerifySolution(ctx context.Context, solution *challenge.Solution) error
GetQuote(ctx context.Context) (*quotes.Quote, error)
}
// WisdomApplication handles the Word of Wisdom application logic
type WisdomApplication struct {
wisdomService WisdomService
}
// NewWisdomApplication creates a new wisdom application handler
func NewWisdomApplication(wisdomService WisdomService) *WisdomApplication {
return &WisdomApplication{
wisdomService: wisdomService,
}
}
// HandleMessage processes a protocol message and returns an encodable response
func (a *WisdomApplication) HandleMessage(ctx context.Context, msg *protocol.Message) (Response, error) {
switch msg.Type {
case protocol.ChallengeRequestType:
return a.handleChallengeRequest(ctx)
case protocol.SolutionRequestType:
return a.handleSolutionRequest(ctx, msg)
default:
return &protocol.ErrorResponse{
Code: protocol.ErrMalformedMessage,
Message: fmt.Sprintf("unsupported message type: 0x%02x", msg.Type),
}, nil
}
}
// handleChallengeRequest processes challenge requests
func (a *WisdomApplication) handleChallengeRequest(ctx context.Context) (Response, error) {
challenge, err := a.wisdomService.GenerateChallenge(ctx, "quotes")
if err != nil {
return &protocol.ErrorResponse{Code: protocol.ErrServerError, Message: "Contact administrator"}, nil
}
return &protocol.ChallengeResponse{Challenge: challenge}, nil
}
// handleSolutionRequest processes solution requests
func (a *WisdomApplication) handleSolutionRequest(ctx context.Context, msg *protocol.Message) (Response, error) {
// Parse solution request
var solutionReq protocol.SolutionRequest
if err := solutionReq.Decode(msg.PayloadStream); err != nil {
return &protocol.ErrorResponse{Code: protocol.ErrMalformedMessage, Message: "invalid solution format"}, nil
}
// Create solution object
solution := &challenge.Solution{
Challenge: solutionReq.Challenge,
Nonce: solutionReq.Nonce,
}
// Verify solution
if err := a.wisdomService.VerifySolution(ctx, solution); err != nil {
return &protocol.ErrorResponse{Code: protocol.ErrInvalidSolution, Message: "solution verification failed"}, nil
}
// Get quote
quote, err := a.wisdomService.GetQuote(ctx)
if err != nil {
return &protocol.ErrorResponse{Code: protocol.ErrServerError, Message: "Contact administrator"}, nil
}
return &protocol.SolutionResponse{Quote: quote}, nil
}

View file

@ -0,0 +1,324 @@
package application
import (
"bytes"
"context"
"encoding/json"
"errors"
"testing"
"hash-of-wisdom/internal/application/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 TestNewWisdomApplication(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
assert.NotNil(t, app)
assert.Equal(t, mockService, app.wisdomService)
}
func TestWisdomApplication_HandleMessage_UnsupportedType(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
ctx := context.Background()
msg := &protocol.Message{
Type: protocol.MessageType(0xFF), // Invalid type
PayloadLength: 0,
PayloadStream: nil,
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ErrorResponse
errorResponse, ok := response.(*protocol.ErrorResponse)
require.True(t, ok, "Expected ErrorResponse")
assert.Equal(t, protocol.ErrMalformedMessage, errorResponse.Code)
assert.Contains(t, errorResponse.Message, "unsupported message type: 0xff")
}
func TestWisdomApplication_HandleChallengeRequest_Success(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(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.ChallengeRequestType,
PayloadLength: 0,
PayloadStream: nil,
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ChallengeResponse
challengeResponse, ok := response.(*protocol.ChallengeResponse)
require.True(t, ok, "Expected ChallengeResponse")
assert.Equal(t, testChallenge, challengeResponse.Challenge)
mockService.AssertExpectations(t)
}
func TestWisdomApplication_HandleChallengeRequest_ServiceError(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
// Mock service error
mockService.On("GenerateChallenge", mock.Anything, "quotes").Return(nil, errors.New("service error"))
ctx := context.Background()
msg := &protocol.Message{
Type: protocol.ChallengeRequestType,
PayloadLength: 0,
PayloadStream: nil,
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ErrorResponse
errorResponse, ok := response.(*protocol.ErrorResponse)
require.True(t, ok, "Expected ErrorResponse")
assert.Equal(t, protocol.ErrServerError, errorResponse.Code)
assert.Equal(t, "Contact administrator", errorResponse.Message)
mockService.AssertExpectations(t)
}
func TestWisdomApplication_HandleSolutionRequest_Success(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
// Create test solution request
testChallenge := challenge.Challenge{
Resource: "quotes",
Timestamp: 12345,
Difficulty: 4,
Random: []byte("test"),
HMAC: []byte("signature"),
}
solutionPayload := protocol.SolutionRequest{
Challenge: testChallenge,
Nonce: 12345,
}
payloadJSON, err := json.Marshal(solutionPayload)
require.NoError(t, err)
testQuote := &quotes.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.SolutionRequestType,
PayloadLength: uint32(len(payloadJSON)),
PayloadStream: bytes.NewReader(payloadJSON),
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to SolutionResponse
solutionResponse, ok := response.(*protocol.SolutionResponse)
require.True(t, ok, "Expected SolutionResponse")
assert.Equal(t, testQuote, solutionResponse.Quote)
mockService.AssertExpectations(t)
}
func TestWisdomApplication_HandleSolutionRequest_InvalidJSON(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
invalidJSON := []byte("invalid json")
ctx := context.Background()
msg := &protocol.Message{
Type: protocol.SolutionRequestType,
PayloadLength: uint32(len(invalidJSON)),
PayloadStream: bytes.NewReader(invalidJSON),
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ErrorResponse
errorResponse, ok := response.(*protocol.ErrorResponse)
require.True(t, ok, "Expected ErrorResponse")
assert.Equal(t, protocol.ErrMalformedMessage, errorResponse.Code)
assert.Equal(t, "invalid solution format", errorResponse.Message)
mockService.AssertNotCalled(t, "VerifySolution")
mockService.AssertNotCalled(t, "GetQuote")
}
func TestWisdomApplication_HandleSolutionRequest_VerificationFailed(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
// Create test solution request
testChallenge := challenge.Challenge{
Resource: "quotes",
Timestamp: 12345,
Difficulty: 4,
Random: []byte("test"),
HMAC: []byte("signature"),
}
solutionPayload := protocol.SolutionRequest{
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.SolutionRequestType,
PayloadLength: uint32(len(payloadJSON)),
PayloadStream: bytes.NewReader(payloadJSON),
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ErrorResponse
errorResponse, ok := response.(*protocol.ErrorResponse)
require.True(t, ok, "Expected ErrorResponse")
assert.Equal(t, protocol.ErrInvalidSolution, errorResponse.Code)
assert.Equal(t, "solution verification failed", errorResponse.Message)
mockService.AssertExpectations(t)
mockService.AssertNotCalled(t, "GetQuote")
}
func TestWisdomApplication_HandleSolutionRequest_QuoteServiceError(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(mockService)
// Create test solution request
testChallenge := challenge.Challenge{
Resource: "quotes",
Timestamp: 12345,
Difficulty: 4,
Random: []byte("test"),
HMAC: []byte("signature"),
}
solutionPayload := protocol.SolutionRequest{
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.SolutionRequestType,
PayloadLength: uint32(len(payloadJSON)),
PayloadStream: bytes.NewReader(payloadJSON),
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ErrorResponse
errorResponse, ok := response.(*protocol.ErrorResponse)
require.True(t, ok, "Expected ErrorResponse")
assert.Equal(t, protocol.ErrServerError, errorResponse.Code)
assert.Equal(t, "Contact administrator", errorResponse.Message)
mockService.AssertExpectations(t)
}
func TestWisdomApplication_HandleMessage_ContextCancellation(t *testing.T) {
mockService := mocks.NewMockWisdomService(t)
app := NewWisdomApplication(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.ChallengeRequestType,
PayloadLength: 0,
PayloadStream: nil,
}
response, err := app.HandleMessage(ctx, msg)
require.NoError(t, err)
// Type assert to ErrorResponse
errorResponse, ok := response.(*protocol.ErrorResponse)
require.True(t, ok, "Expected ErrorResponse")
assert.Equal(t, protocol.ErrServerError, errorResponse.Code)
mockService.AssertExpectations(t)
}
func TestResponseEncoding(t *testing.T) {
// Test ChallengeResponse encoding produces valid binary format
testChallenge := &challenge.Challenge{
Resource: "quotes",
Timestamp: 12345,
Difficulty: 4,
Random: []byte("test"),
HMAC: []byte("signature"),
}
challengeResponse := &protocol.ChallengeResponse{Challenge: testChallenge}
var buf bytes.Buffer
err := challengeResponse.Encode(&buf)
require.NoError(t, err)
// Verify binary format
data := buf.Bytes()
assert.GreaterOrEqual(t, len(data), 5) // At least header size
// Check message type
assert.Equal(t, byte(protocol.ChallengeResponseType), data[0])
// Check payload contains expected data
payload := string(data[5:]) // Skip header
assert.Contains(t, payload, "quotes")
assert.Contains(t, payload, "12345")
}

View file

@ -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
}