You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							299 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							299 lines
						
					
					
						
							11 KiB
						
					
					
				| package integration | |
| 
 | |
| import ( | |
| 	"encoding/json" | |
| 	"fmt" | |
| 	"net/http" | |
| 	"net/http/httptest" | |
| 	"testing" | |
| 
 | |
| 	"github.com/linkedin/goavro/v2" | |
| 	"github.com/stretchr/testify/assert" | |
| 	"github.com/stretchr/testify/require" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/kafka/schema" | |
| ) | |
| 
 | |
| // TestSchemaEndToEnd_AvroRoundTrip tests the complete Avro schema round-trip workflow | |
| func TestSchemaEndToEnd_AvroRoundTrip(t *testing.T) { | |
| 	// Create mock schema registry | |
| 	server := createMockSchemaRegistryForE2E(t) | |
| 	defer server.Close() | |
| 
 | |
| 	// Create schema manager | |
| 	config := schema.ManagerConfig{ | |
| 		RegistryURL:    server.URL, | |
| 		ValidationMode: schema.ValidationPermissive, | |
| 	} | |
| 	manager, err := schema.NewManager(config) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	// Test data | |
| 	avroSchema := getUserAvroSchemaForE2E() | |
| 	testData := map[string]interface{}{ | |
| 		"id":    int32(12345), | |
| 		"name":  "Alice Johnson", | |
| 		"email": map[string]interface{}{"string": "alice@example.com"}, // Avro union | |
| 		"age":   map[string]interface{}{"int": int32(28)},              // Avro union | |
| 		"preferences": map[string]interface{}{ | |
| 			"Preferences": map[string]interface{}{ // Avro union with record type | |
| 				"notifications": true, | |
| 				"theme":         "dark", | |
| 			}, | |
| 		}, | |
| 	} | |
| 
 | |
| 	t.Run("SchemaManagerRoundTrip", func(t *testing.T) { | |
| 		// Step 1: Create Confluent envelope (simulate producer) | |
| 		codec, err := goavro.NewCodec(avroSchema) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		avroBinary, err := codec.BinaryFromNative(nil, testData) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		confluentMsg := schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, avroBinary) | |
| 		require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty") | |
| 
 | |
| 		t.Logf("Created Confluent envelope: %d bytes", len(confluentMsg)) | |
| 
 | |
| 		// Step 2: Decode message using schema manager | |
| 		decodedMsg, err := manager.DecodeMessage(confluentMsg) | |
| 		require.NoError(t, err) | |
| 		require.NotNil(t, decodedMsg.RecordValue, "RecordValue should not be nil") | |
| 
 | |
| 		t.Logf("Decoded message with schema ID %d, format %v", decodedMsg.SchemaID, decodedMsg.SchemaFormat) | |
| 
 | |
| 		// Step 3: Re-encode message using schema manager | |
| 		reconstructedMsg, err := manager.EncodeMessage(decodedMsg.RecordValue, 1, schema.FormatAvro) | |
| 		require.NoError(t, err) | |
| 		require.True(t, len(reconstructedMsg) > 0, "Reconstructed message should not be empty") | |
| 
 | |
| 		t.Logf("Re-encoded message: %d bytes", len(reconstructedMsg)) | |
| 
 | |
| 		// Step 4: Verify the reconstructed message is a valid Confluent envelope | |
| 		envelope, ok := schema.ParseConfluentEnvelope(reconstructedMsg) | |
| 		require.True(t, ok, "Reconstructed message should be a valid Confluent envelope") | |
| 		require.Equal(t, uint32(1), envelope.SchemaID, "Schema ID should match") | |
| 		require.Equal(t, schema.FormatAvro, envelope.Format, "Schema format should be Avro") | |
| 
 | |
| 		// Step 5: Decode and verify the content | |
| 		decodedNative, _, err := codec.NativeFromBinary(envelope.Payload) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		decodedMap, ok := decodedNative.(map[string]interface{}) | |
| 		require.True(t, ok, "Decoded data should be a map") | |
| 
 | |
| 		// Verify all fields | |
| 		assert.Equal(t, int32(12345), decodedMap["id"]) | |
| 		assert.Equal(t, "Alice Johnson", decodedMap["name"]) | |
| 		 | |
| 		// Verify union fields | |
| 		emailUnion, ok := decodedMap["email"].(map[string]interface{}) | |
| 		require.True(t, ok, "Email should be a union") | |
| 		assert.Equal(t, "alice@example.com", emailUnion["string"]) | |
| 
 | |
| 		ageUnion, ok := decodedMap["age"].(map[string]interface{}) | |
| 		require.True(t, ok, "Age should be a union") | |
| 		assert.Equal(t, int32(28), ageUnion["int"]) | |
| 
 | |
| 		preferencesUnion, ok := decodedMap["preferences"].(map[string]interface{}) | |
| 		require.True(t, ok, "Preferences should be a union") | |
| 		preferencesRecord, ok := preferencesUnion["Preferences"].(map[string]interface{}) | |
| 		require.True(t, ok, "Preferences should contain a record") | |
| 		assert.Equal(t, true, preferencesRecord["notifications"]) | |
| 		assert.Equal(t, "dark", preferencesRecord["theme"]) | |
| 
 | |
| 		t.Log("Successfully completed Avro schema round-trip test") | |
| 	}) | |
| } | |
| 
 | |
| // TestSchemaEndToEnd_ProtobufRoundTrip tests the complete Protobuf schema round-trip workflow | |
| func TestSchemaEndToEnd_ProtobufRoundTrip(t *testing.T) { | |
| 	t.Run("ProtobufEnvelopeCreation", func(t *testing.T) { | |
| 		// Create a simple Protobuf message (simulated) | |
| 		// In a real scenario, this would be generated from a .proto file | |
| 		protobufData := []byte{0x08, 0x96, 0x01, 0x12, 0x04, 0x74, 0x65, 0x73, 0x74} // id=150, name="test" | |
|  | |
| 		// Create Confluent envelope with Protobuf format | |
| 		confluentMsg := schema.CreateConfluentEnvelope(schema.FormatProtobuf, 2, []int{0}, protobufData) | |
| 		require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty") | |
| 
 | |
| 		t.Logf("Created Protobuf Confluent envelope: %d bytes", len(confluentMsg)) | |
| 
 | |
| 		// Verify Confluent envelope | |
| 		envelope, ok := schema.ParseConfluentEnvelope(confluentMsg) | |
| 		require.True(t, ok, "Message should be a valid Confluent envelope") | |
| 		require.Equal(t, uint32(2), envelope.SchemaID, "Schema ID should match") | |
| 		// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry | |
| 		require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup") | |
| 		 | |
| 		// For Protobuf with indexes, we need to use the specialized parser | |
| 		protobufEnvelope, ok := schema.ParseConfluentProtobufEnvelopeWithIndexCount(confluentMsg, 1) | |
| 		require.True(t, ok, "Message should be a valid Protobuf envelope") | |
| 		require.Equal(t, uint32(2), protobufEnvelope.SchemaID, "Schema ID should match") | |
| 		require.Equal(t, schema.FormatProtobuf, protobufEnvelope.Format, "Schema format should be Protobuf") | |
| 		require.Equal(t, []int{0}, protobufEnvelope.Indexes, "Indexes should match") | |
| 		require.Equal(t, protobufData, protobufEnvelope.Payload, "Payload should match") | |
| 
 | |
| 		t.Log("Successfully completed Protobuf envelope test") | |
| 	}) | |
| } | |
| 
 | |
| // TestSchemaEndToEnd_JSONSchemaRoundTrip tests the complete JSON Schema round-trip workflow | |
| func TestSchemaEndToEnd_JSONSchemaRoundTrip(t *testing.T) { | |
| 	t.Run("JSONSchemaEnvelopeCreation", func(t *testing.T) { | |
| 		// Create JSON data | |
| 		jsonData := []byte(`{"id": 123, "name": "Bob Smith", "active": true}`) | |
| 
 | |
| 		// Create Confluent envelope with JSON Schema format | |
| 		confluentMsg := schema.CreateConfluentEnvelope(schema.FormatJSONSchema, 3, nil, jsonData) | |
| 		require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty") | |
| 
 | |
| 		t.Logf("Created JSON Schema Confluent envelope: %d bytes", len(confluentMsg)) | |
| 
 | |
| 		// Verify Confluent envelope | |
| 		envelope, ok := schema.ParseConfluentEnvelope(confluentMsg) | |
| 		require.True(t, ok, "Message should be a valid Confluent envelope") | |
| 		require.Equal(t, uint32(3), envelope.SchemaID, "Schema ID should match") | |
| 		// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry | |
| 		require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup") | |
| 
 | |
| 		// Verify JSON content | |
| 		assert.JSONEq(t, string(jsonData), string(envelope.Payload), "JSON payload should match") | |
| 
 | |
| 		t.Log("Successfully completed JSON Schema envelope test") | |
| 	}) | |
| } | |
| 
 | |
| // TestSchemaEndToEnd_CompressionAndBatching tests schema handling with compression and batching | |
| func TestSchemaEndToEnd_CompressionAndBatching(t *testing.T) { | |
| 	// Create mock schema registry | |
| 	server := createMockSchemaRegistryForE2E(t) | |
| 	defer server.Close() | |
| 
 | |
| 	// Create schema manager | |
| 	config := schema.ManagerConfig{ | |
| 		RegistryURL:    server.URL, | |
| 		ValidationMode: schema.ValidationPermissive, | |
| 	} | |
| 	manager, err := schema.NewManager(config) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	t.Run("BatchedSchematizedMessages", func(t *testing.T) { | |
| 		// Create multiple messages | |
| 		avroSchema := getUserAvroSchemaForE2E() | |
| 		codec, err := goavro.NewCodec(avroSchema) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		messageCount := 5 | |
| 		var confluentMessages [][]byte | |
| 
 | |
| 		// Create multiple Confluent envelopes | |
| 		for i := 0; i < messageCount; i++ { | |
| 			testData := map[string]interface{}{ | |
| 				"id":    int32(1000 + i), | |
| 				"name":  fmt.Sprintf("User %d", i), | |
| 				"email": map[string]interface{}{"string": fmt.Sprintf("user%d@example.com", i)}, | |
| 				"age":   map[string]interface{}{"int": int32(20 + i)}, | |
| 				"preferences": map[string]interface{}{ | |
| 					"Preferences": map[string]interface{}{ | |
| 						"notifications": i%2 == 0, // Alternate true/false | |
| 						"theme":         "light", | |
| 					}, | |
| 				}, | |
| 			} | |
| 
 | |
| 			avroBinary, err := codec.BinaryFromNative(nil, testData) | |
| 			require.NoError(t, err) | |
| 
 | |
| 			confluentMsg := schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, avroBinary) | |
| 			confluentMessages = append(confluentMessages, confluentMsg) | |
| 		} | |
| 
 | |
| 		t.Logf("Created %d schematized messages", messageCount) | |
| 
 | |
| 		// Test round-trip for each message | |
| 		for i, confluentMsg := range confluentMessages { | |
| 			// Decode message | |
| 			decodedMsg, err := manager.DecodeMessage(confluentMsg) | |
| 			require.NoError(t, err, "Message %d should decode", i) | |
| 
 | |
| 			// Re-encode message | |
| 			reconstructedMsg, err := manager.EncodeMessage(decodedMsg.RecordValue, 1, schema.FormatAvro) | |
| 			require.NoError(t, err, "Message %d should re-encode", i) | |
| 
 | |
| 			// Verify envelope | |
| 			envelope, ok := schema.ParseConfluentEnvelope(reconstructedMsg) | |
| 			require.True(t, ok, "Message %d should be a valid Confluent envelope", i) | |
| 			require.Equal(t, uint32(1), envelope.SchemaID, "Message %d schema ID should match", i) | |
| 
 | |
| 			// Decode and verify content | |
| 			decodedNative, _, err := codec.NativeFromBinary(envelope.Payload) | |
| 			require.NoError(t, err, "Message %d should decode successfully", i) | |
| 
 | |
| 			decodedMap, ok := decodedNative.(map[string]interface{}) | |
| 			require.True(t, ok, "Message %d should be a map", i) | |
| 
 | |
| 			expectedID := int32(1000 + i) | |
| 			assert.Equal(t, expectedID, decodedMap["id"], "Message %d ID should match", i) | |
| 			assert.Equal(t, fmt.Sprintf("User %d", i), decodedMap["name"], "Message %d name should match", i) | |
| 		} | |
| 
 | |
| 		t.Log("Successfully verified batched schematized messages") | |
| 	}) | |
| } | |
| 
 | |
| // Helper functions for creating mock schema registries | |
|  | |
| func createMockSchemaRegistryForE2E(t *testing.T) *httptest.Server { | |
| 	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 		switch r.URL.Path { | |
| 		case "/schemas/ids/1": | |
| 			response := map[string]interface{}{ | |
| 				"schema":  getUserAvroSchemaForE2E(), | |
| 				"subject": "user-events-e2e-value", | |
| 				"version": 1, | |
| 			} | |
| 			writeJSONResponse(w, response) | |
| 		case "/subjects/user-events-e2e-value/versions/latest": | |
| 			response := map[string]interface{}{ | |
| 				"id":      1, | |
| 				"schema":  getUserAvroSchemaForE2E(), | |
| 				"subject": "user-events-e2e-value", | |
| 				"version": 1, | |
| 			} | |
| 			writeJSONResponse(w, response) | |
| 		default: | |
| 			w.WriteHeader(http.StatusNotFound) | |
| 		} | |
| 	})) | |
| } | |
| 
 | |
| 
 | |
| func getUserAvroSchemaForE2E() string { | |
| 	return `{ | |
| 		"type": "record", | |
| 		"name": "User", | |
| 		"fields": [ | |
| 			{"name": "id", "type": "int"}, | |
| 			{"name": "name", "type": "string"}, | |
| 			{"name": "email", "type": ["null", "string"], "default": null}, | |
| 			{"name": "age", "type": ["null", "int"], "default": null}, | |
| 			{"name": "preferences", "type": ["null", { | |
| 				"type": "record", | |
| 				"name": "Preferences", | |
| 				"fields": [ | |
| 					{"name": "notifications", "type": "boolean", "default": true}, | |
| 					{"name": "theme", "type": "string", "default": "light"} | |
| 				] | |
| 			}], "default": null} | |
| 		] | |
| 	}` | |
| } | |
| 
 | |
| func writeJSONResponse(w http.ResponseWriter, data interface{}) { | |
| 	w.Header().Set("Content-Type", "application/json") | |
| 	if err := json.NewEncoder(w).Encode(data); err != nil { | |
| 		http.Error(w, err.Error(), http.StatusInternalServerError) | |
| 	} | |
| }
 |