diff --git a/internal/protocol/encode_decode_test.go b/internal/protocol/encode_decode_test.go new file mode 100644 index 0000000..b302a47 --- /dev/null +++ b/internal/protocol/encode_decode_test.go @@ -0,0 +1,157 @@ +package protocol + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "hash-of-wisdom/internal/pow/challenge" + "hash-of-wisdom/internal/quotes" +) + +func TestSymmetricEncoding_ChallengeRequest(t *testing.T) { + // Create a challenge request + req := &ChallengeRequest{} + + // Encode it + var buf bytes.Buffer + err := req.Encode(&buf) + require.NoError(t, err) + + // Decode it back + decoder := NewMessageDecoder() + msg, err := decoder.Decode(&buf) + require.NoError(t, err) + assert.Equal(t, ChallengeRequestType, msg.Type) + assert.Equal(t, uint32(0), msg.PayloadLength) + + // Decode the request payload + decodedReq := &ChallengeRequest{} + err = decodedReq.Decode(msg.PayloadStream) + require.NoError(t, err) +} + +func TestSymmetricEncoding_SolutionRequest(t *testing.T) { + // Create a solution request + req := &SolutionRequest{ + Challenge: challenge.Challenge{ + Timestamp: 1640995200, + Difficulty: 4, + Resource: "quotes", + Random: []byte("test"), + HMAC: []byte("test"), + }, + Nonce: 12345, + } + + // Encode it + var buf bytes.Buffer + err := req.Encode(&buf) + require.NoError(t, err) + + // Decode it back + decoder := NewMessageDecoder() + msg, err := decoder.Decode(&buf) + require.NoError(t, err) + assert.Equal(t, SolutionRequestType, msg.Type) + assert.Greater(t, msg.PayloadLength, uint32(0)) + + // Decode the request payload + decodedReq := &SolutionRequest{} + err = decodedReq.Decode(msg.PayloadStream) + require.NoError(t, err) + assert.Equal(t, req.Nonce, decodedReq.Nonce) + assert.Equal(t, req.Challenge.Timestamp, decodedReq.Challenge.Timestamp) +} + +func TestSymmetricEncoding_ChallengeResponse(t *testing.T) { + // Create a challenge response + resp := &ChallengeResponse{ + Challenge: &challenge.Challenge{ + Timestamp: 1640995200, + Difficulty: 4, + Resource: "quotes", + Random: []byte("test"), + HMAC: []byte("test"), + }, + } + + // Encode it + var buf bytes.Buffer + err := resp.Encode(&buf) + require.NoError(t, err) + + // Decode it back + decoder := NewMessageDecoder() + msg, err := decoder.Decode(&buf) + require.NoError(t, err) + assert.Equal(t, ChallengeResponseType, msg.Type) + assert.Greater(t, msg.PayloadLength, uint32(0)) + + // Decode the response payload + decodedResp := &ChallengeResponse{} + err = decodedResp.Decode(msg.PayloadStream) + require.NoError(t, err) + assert.Equal(t, resp.Challenge.Timestamp, decodedResp.Challenge.Timestamp) + assert.Equal(t, resp.Challenge.Difficulty, decodedResp.Challenge.Difficulty) +} + +func TestSymmetricEncoding_SolutionResponse(t *testing.T) { + // Create a solution response + resp := &SolutionResponse{ + Quote: "es.Quote{ + Text: "Test quote", + Author: "Test Author", + }, + } + + // Encode it + var buf bytes.Buffer + err := resp.Encode(&buf) + require.NoError(t, err) + + // Decode it back + decoder := NewMessageDecoder() + msg, err := decoder.Decode(&buf) + require.NoError(t, err) + assert.Equal(t, QuoteResponseType, msg.Type) + assert.Greater(t, msg.PayloadLength, uint32(0)) + + // Decode the response payload + decodedResp := &SolutionResponse{} + err = decodedResp.Decode(msg.PayloadStream) + require.NoError(t, err) + assert.Equal(t, resp.Quote.Text, decodedResp.Quote.Text) + assert.Equal(t, resp.Quote.Author, decodedResp.Quote.Author) +} + +func TestSymmetricEncoding_ErrorResponse(t *testing.T) { + // Create an error response + resp := &ErrorResponse{ + Code: "TEST_ERROR", + Message: "Test error message", + Details: map[string]string{"key": "value"}, + } + + // Encode it + var buf bytes.Buffer + err := resp.Encode(&buf) + require.NoError(t, err) + + // Decode it back + decoder := NewMessageDecoder() + msg, err := decoder.Decode(&buf) + require.NoError(t, err) + assert.Equal(t, ErrorResponseType, msg.Type) + assert.Greater(t, msg.PayloadLength, uint32(0)) + + // Decode the response payload + decodedResp := &ErrorResponse{} + err = decodedResp.Decode(msg.PayloadStream) + require.NoError(t, err) + assert.Equal(t, resp.Code, decodedResp.Code) + assert.Equal(t, resp.Message, decodedResp.Message) + assert.Equal(t, resp.Details, decodedResp.Details) +} diff --git a/internal/protocol/roundtrip_test.go b/internal/protocol/roundtrip_test.go index d32f3d8..ef4b5fa 100644 --- a/internal/protocol/roundtrip_test.go +++ b/internal/protocol/roundtrip_test.go @@ -113,16 +113,6 @@ func TestChallengeRequest_EmptyPayload(t *testing.T) { require.NoError(t, err) } -func TestMessageDecoder_RejectsResponseTypes(t *testing.T) { - decoder := NewMessageDecoder() - - data := []byte{byte(ErrorResponseType), 0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o'} - buf := bytes.NewBuffer(data) - - _, err := decoder.Decode(buf) - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid message type") -} func TestPayloadStream_LimitedRead(t *testing.T) { decoder := NewMessageDecoder() diff --git a/internal/protocol/spec_compliance_test.go b/internal/protocol/spec_compliance_test.go new file mode 100644 index 0000000..4eef5ae --- /dev/null +++ b/internal/protocol/spec_compliance_test.go @@ -0,0 +1,177 @@ +package protocol + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "hash-of-wisdom/internal/pow/challenge" + "hash-of-wisdom/internal/quotes" +) + +// TestSpecCompliance_ChallengeResponse verifies challenge response matches PROTOCOL.md format +func TestSpecCompliance_ChallengeResponse(t *testing.T) { + resp := &ChallengeResponse{ + Challenge: &challenge.Challenge{ + Timestamp: 1640995200, + Difficulty: 4, + Resource: "quotes", + Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, + HMAC: []byte("base64url_encoded_signature"), + }, + } + + var buf bytes.Buffer + err := resp.Encode(&buf) + require.NoError(t, err) + + // Skip header (5 bytes) and get payload + header := buf.Bytes()[:5] + payload := buf.Bytes()[5:] + + // Verify header format + assert.Equal(t, byte(ChallengeResponseType), header[0]) + assert.Equal(t, uint32(len(payload)), uint32(header[1])<<24|uint32(header[2])<<16|uint32(header[3])<<8|uint32(header[4])) + + // Verify JSON payload matches spec format + var decoded map[string]interface{} + err = json.Unmarshal(payload, &decoded) + require.NoError(t, err) + + // Check required fields from spec + assert.Contains(t, decoded, "timestamp") + assert.Contains(t, decoded, "difficulty") + assert.Contains(t, decoded, "resource") + assert.Contains(t, decoded, "random") + assert.Contains(t, decoded, "hmac") + + assert.Equal(t, float64(1640995200), decoded["timestamp"]) + assert.Equal(t, float64(4), decoded["difficulty"]) + assert.Equal(t, "quotes", decoded["resource"]) +} + +// TestSpecCompliance_SolutionRequest verifies solution request matches PROTOCOL.md format +func TestSpecCompliance_SolutionRequest(t *testing.T) { + req := &SolutionRequest{ + Challenge: challenge.Challenge{ + Timestamp: 1640995200, + Difficulty: 4, + Resource: "quotes", + Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, + HMAC: []byte("base64url_encoded_signature"), + }, + Nonce: 12345, + } + + var buf bytes.Buffer + err := req.Encode(&buf) + require.NoError(t, err) + + // Skip header and get payload + payload := buf.Bytes()[5:] + + // Verify JSON payload matches spec format + var decoded map[string]interface{} + err = json.Unmarshal(payload, &decoded) + require.NoError(t, err) + + // Check required top-level fields + assert.Contains(t, decoded, "challenge") + assert.Contains(t, decoded, "nonce") + assert.Equal(t, float64(12345), decoded["nonce"]) + + // Check challenge structure + challenge := decoded["challenge"].(map[string]interface{}) + assert.Contains(t, challenge, "timestamp") + assert.Contains(t, challenge, "difficulty") + assert.Contains(t, challenge, "resource") + assert.Contains(t, challenge, "random") + assert.Contains(t, challenge, "hmac") +} + +// TestSpecCompliance_QuoteResponse verifies quote response matches PROTOCOL.md format +func TestSpecCompliance_QuoteResponse(t *testing.T) { + resp := &SolutionResponse{ + Quote: "es.Quote{ + Text: "The only way to do great work is to love what you do.", + Author: "Steve Jobs", + }, + } + + var buf bytes.Buffer + err := resp.Encode(&buf) + require.NoError(t, err) + + // Skip header and get payload + payload := buf.Bytes()[5:] + + // Verify JSON payload matches spec format + var decoded map[string]interface{} + err = json.Unmarshal(payload, &decoded) + require.NoError(t, err) + + // Check required fields from spec + assert.Contains(t, decoded, "text") + assert.Contains(t, decoded, "author") + assert.Equal(t, "The only way to do great work is to love what you do.", decoded["text"]) + assert.Equal(t, "Steve Jobs", decoded["author"]) +} + +// TestSpecCompliance_ErrorResponse verifies error response matches PROTOCOL.md format +func TestSpecCompliance_ErrorResponse(t *testing.T) { + resp := &ErrorResponse{ + Code: "INVALID_SOLUTION", + Message: "The provided PoW solution is incorrect", + RetryAfter: 30, + Details: map[string]string{"reason": "hash verification failed"}, + } + + var buf bytes.Buffer + err := resp.Encode(&buf) + require.NoError(t, err) + + // Skip header and get payload + payload := buf.Bytes()[5:] + + // Verify JSON payload matches spec format + var decoded map[string]interface{} + err = json.Unmarshal(payload, &decoded) + require.NoError(t, err) + + // Check required fields from spec + assert.Contains(t, decoded, "code") + assert.Contains(t, decoded, "message") + assert.Equal(t, "INVALID_SOLUTION", decoded["code"]) + assert.Equal(t, "The provided PoW solution is incorrect", decoded["message"]) + + // Check optional fields + assert.Contains(t, decoded, "retry_after") + assert.Contains(t, decoded, "details") + assert.Equal(t, float64(30), decoded["retry_after"]) +} + +// TestSpecCompliance_MessageSizeLimits verifies 8KB payload limit +func TestSpecCompliance_MessageSizeLimits(t *testing.T) { + decoder := NewMessageDecoder() + + // Create a message that exceeds 8KB payload limit + largePayload := make([]byte, MaxPayloadSize+1) + for i := range largePayload { + largePayload[i] = 'A' + } + + // Create message with oversized payload + var buf bytes.Buffer + buf.WriteByte(byte(ChallengeRequestType)) + buf.Write([]byte{0x00, 0x00, 0x20, 0x01}) // 8193 bytes (8KB + 1) + buf.Write(largePayload) + + // Should reject oversized payload + _, err := decoder.Decode(&buf) + assert.Error(t, err) + assert.Contains(t, err.Error(), "payload length") + assert.Contains(t, err.Error(), "exceeds maximum") +}