github.com/kaptinlin/jsonschema@v0.4.6/compiler_test.go (about)

     1  package jsonschema
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  )
    14  
    15  const (
    16  	remoteSchemaURL = "https://json-schema.org/draft/2020-12/schema"
    17  )
    18  
    19  func TestCompileWithID(t *testing.T) {
    20  	compiler := NewCompiler()
    21  	schemaJSON := createTestSchemaJSON("http://example.com/schema", map[string]string{"name": "string"}, []string{"name"})
    22  
    23  	schema, err := compiler.Compile([]byte(schemaJSON))
    24  	require.NoError(t, err, "Failed to compile schema with $id")
    25  
    26  	assert.Equal(t, "http://example.com/schema", schema.ID, "Expected $id to be 'http://example.com/schema'")
    27  }
    28  
    29  func TestGetSchema(t *testing.T) {
    30  	compiler := NewCompiler()
    31  	schemaJSON := createTestSchemaJSON("http://example.com/schema", map[string]string{"name": "string"}, []string{"name"})
    32  	_, err := compiler.Compile([]byte(schemaJSON))
    33  	require.NoError(t, err, "Failed to compile schema")
    34  
    35  	schema, err := compiler.GetSchema("http://example.com/schema")
    36  	require.NoError(t, err, "Failed to retrieve compiled schema")
    37  
    38  	assert.Equal(t, "http://example.com/schema", schema.ID, "Expected to retrieve schema with $id 'http://example.com/schema'")
    39  }
    40  
    41  func TestValidateRemoteSchema(t *testing.T) {
    42  	compiler := NewCompiler()
    43  
    44  	// Load the meta-schema
    45  	metaSchema, err := compiler.GetSchema(remoteSchemaURL)
    46  	require.NoError(t, err, "Failed to load meta-schema")
    47  
    48  	// Ensure that the schema is not nil
    49  	require.NotNil(t, metaSchema, "Meta-schema is nil")
    50  
    51  	// Verify the ID of the retrieved schema
    52  	expectedID := remoteSchemaURL
    53  	assert.Equal(t, expectedID, metaSchema.ID, "Expected schema with ID %s", expectedID)
    54  }
    55  
    56  func TestCompileCache(t *testing.T) {
    57  	compiler := NewCompiler()
    58  	schemaJSON := createTestSchemaJSON("http://example.com/schema", map[string]string{"name": "string"}, []string{"name"})
    59  	_, err := compiler.Compile([]byte(schemaJSON))
    60  	require.NoError(t, err, "Failed to compile schema")
    61  
    62  	// Attempt to compile the same schema again
    63  	_, err = compiler.Compile([]byte(schemaJSON))
    64  	require.NoError(t, err, "Failed to compile schema a second time")
    65  
    66  	assert.Len(t, compiler.schemas, 1, "Schema should be compiled once and cached")
    67  }
    68  
    69  func TestResolveReferences(t *testing.T) {
    70  	compiler := NewCompiler()
    71  	// Assuming this schema is already compiled and cached
    72  	baseSchemaJSON := createTestSchemaJSON("http://example.com/base", map[string]string{"age": "integer"}, nil)
    73  	_, err := compiler.Compile([]byte(baseSchemaJSON))
    74  	require.NoError(t, err, "Failed to compile base schema")
    75  
    76  	refSchemaJSON := `{
    77  		"$id": "http://example.com/ref",
    78  		"type": "object",
    79  		"properties": {
    80  			"userInfo": {"$ref": "http://example.com/base"}
    81  		}
    82  	}`
    83  
    84  	_, err = compiler.Compile([]byte(refSchemaJSON))
    85  	require.NoError(t, err, "Failed to resolve reference")
    86  }
    87  
    88  func TestResolveReferencesCorrectly(t *testing.T) {
    89  	compiler := NewCompiler()
    90  
    91  	// Compile and cache the base schema which will be referenced.
    92  	baseSchemaJSON := `{
    93          "$id": "http://example.com/base",
    94          "type": "object",
    95          "properties": {
    96              "age": {"type": "integer"}
    97          },
    98          "required": ["age"]
    99      }`
   100  	baseSchema, err := compiler.Compile([]byte(baseSchemaJSON))
   101  	require.NoError(t, err, "Failed to compile base schema")
   102  
   103  	// Print base schema ID and check if cached correctly
   104  	cachedBaseSchema, cacheErr := compiler.GetSchema("http://example.com/base")
   105  	require.NoError(t, cacheErr, "Base schema cache retrieval failed")
   106  	require.NotNil(t, cachedBaseSchema, "Base schema not cached correctly")
   107  
   108  	// Compile another schema that references the base schema.
   109  	refSchemaJSON := `{
   110          "$id": "http://example.com/ref",
   111          "type": "object",
   112          "properties": {
   113              "userInfo": {"$ref": "http://example.com/base"}
   114          }
   115      }`
   116  
   117  	refSchema, err := compiler.Compile([]byte(refSchemaJSON))
   118  	require.NoError(t, err, "Failed to compile schema with $ref")
   119  
   120  	// Verify that the $ref in refSchema is correctly resolved to the base schema.
   121  	require.NotNil(t, refSchema.Properties, "Properties map should not be nil")
   122  
   123  	userInfoProp, exists := (*refSchema.Properties)["userInfo"]
   124  	require.True(t, exists, "userInfo property should exist")
   125  	require.NotNil(t, userInfoProp, "userInfo property should have a non-nil Schema")
   126  
   127  	// Assert that ResolvedRef is not nil and correctly points to the base schema
   128  	require.NotNil(t, userInfoProp.ResolvedRef, "ResolvedRef for userInfo should not be nil")
   129  	assert.Same(t, baseSchema, userInfoProp.ResolvedRef, "ResolvedRef for userInfo does not match the base schema")
   130  }
   131  
   132  func TestSetDefaultBaseURI(t *testing.T) {
   133  	compiler := NewCompiler()
   134  	baseURI := "http://example.com/schemas/"
   135  	compiler.SetDefaultBaseURI(baseURI)
   136  
   137  	schemaJSON := createTestSchemaJSON("schema", map[string]string{"name": "string"}, []string{"name"})
   138  	schema, err := compiler.Compile([]byte(schemaJSON))
   139  	require.NoError(t, err, "Failed to compile schema")
   140  
   141  	expectedURI := baseURI + "schema"
   142  	assert.Equal(t, expectedURI, schema.uri, "Expected schema URI to be '%s'", expectedURI)
   143  }
   144  
   145  func TestSetAssertFormat(t *testing.T) {
   146  	compiler := NewCompiler()
   147  	compiler.SetAssertFormat(true)
   148  
   149  	schemaJSON := `{
   150  		"type": "string",
   151  		"format": "email"
   152  	}`
   153  
   154  	schema, err := compiler.Compile([]byte(schemaJSON))
   155  	require.NoError(t, err, "Failed to compile schema")
   156  
   157  	assert.True(t, compiler.AssertFormat, "Expected AssertFormat to be true")
   158  
   159  	result := schema.Validate("not-an-email")
   160  	assert.False(t, result.IsValid(), "Expected validation to fail for invalid email format")
   161  }
   162  
   163  func TestRegisterDecoder(t *testing.T) {
   164  	compiler := NewCompiler()
   165  	testDecoder := func(data string) ([]byte, error) {
   166  		return []byte(strings.ToUpper(data)), nil
   167  	}
   168  	compiler.RegisterDecoder("test", testDecoder)
   169  
   170  	_, exists := compiler.Decoders["test"]
   171  	assert.True(t, exists, "Expected decoder to be registered")
   172  }
   173  
   174  func TestRegisterMediaType(t *testing.T) {
   175  	compiler := NewCompiler()
   176  	testUnmarshaler := func(data []byte) (interface{}, error) {
   177  		return string(data), nil
   178  	}
   179  	compiler.RegisterMediaType("test/type", testUnmarshaler)
   180  
   181  	_, exists := compiler.MediaTypes["test/type"]
   182  	assert.True(t, exists, "Expected media type handler to be registered")
   183  }
   184  
   185  func TestRegisterLoader(t *testing.T) {
   186  	compiler := NewCompiler()
   187  	testLoader := func(url string) (io.ReadCloser, error) {
   188  		return io.NopCloser(strings.NewReader(`{"type": "string"}`)), nil
   189  	}
   190  	compiler.RegisterLoader("test", testLoader)
   191  
   192  	_, exists := compiler.Loaders["test"]
   193  	assert.True(t, exists, "Expected loader to be registered")
   194  }
   195  
   196  // createTestSchemaJSON simplifies creating JSON schema strings for testing.
   197  func createTestSchemaJSON(id string, properties map[string]string, required []string) string {
   198  	propsStr := ""
   199  	for propName, propType := range properties {
   200  		propsStr += fmt.Sprintf(`"%s": {"type": "%s"},`, propName, propType)
   201  	}
   202  	if len(propsStr) > 0 {
   203  		propsStr = propsStr[:len(propsStr)-1] // Remove the trailing comma
   204  	}
   205  
   206  	reqStr := "["
   207  	for _, req := range required {
   208  		reqStr += fmt.Sprintf(`"%s",`, req)
   209  	}
   210  	if len(reqStr) > 1 {
   211  		reqStr = reqStr[:len(reqStr)-1] // Remove the trailing comma
   212  	}
   213  	reqStr += "]"
   214  
   215  	return fmt.Sprintf(`{
   216  		"$id": "%s",
   217  		"type": "object",
   218  		"properties": {%s},
   219  		"required": %s
   220  	}`, id, propsStr, reqStr)
   221  }
   222  
   223  // TestWithEncoderJSON tests the WithEncoderJSON method of the Compiler struct.
   224  func TestWithEncoderJSON(t *testing.T) {
   225  	compiler := NewCompiler()
   226  
   227  	// Custom JSON encoder
   228  	customEncoder := func(v interface{}) ([]byte, error) {
   229  		// Add an encoder with a custom prefix
   230  		defaultBytes, err := json.Marshal(v)
   231  		if err != nil {
   232  			return nil, err
   233  		}
   234  		return append([]byte("custom:"), defaultBytes...), nil
   235  	}
   236  
   237  	// Set the custom encoder
   238  	compiler.WithEncoderJSON(customEncoder)
   239  
   240  	// Test data
   241  	testData := map[string]string{"test": "value"}
   242  
   243  	// Use the custom encoder to encode
   244  	encoded, err := compiler.jsonEncoder(testData)
   245  	require.NoError(t, err, "Failed to encode")
   246  
   247  	// Verify the result
   248  	assert.True(t, strings.HasPrefix(string(encoded), "custom:"), "Expected encoded result to start with 'custom:', got: %s", string(encoded))
   249  }
   250  
   251  func TestWithDecoderJSON(t *testing.T) {
   252  	compiler := NewCompiler()
   253  
   254  	// Custom JSON decoder
   255  	customDecoder := func(data []byte, v interface{}) error {
   256  		// Remove the custom prefix
   257  		if bytes.HasPrefix(data, []byte("custom:")) {
   258  			data = bytes.TrimPrefix(data, []byte("custom:"))
   259  		}
   260  		return json.Unmarshal(data, v)
   261  	}
   262  
   263  	// Set the custom decoder
   264  	compiler.WithDecoderJSON(customDecoder)
   265  
   266  	// Test data
   267  	inputJSON := []byte(`custom:{"test":"value"}`)
   268  	var result map[string]string
   269  
   270  	// Use the custom decoder to decode
   271  	err := compiler.jsonDecoder(inputJSON, &result)
   272  	require.NoError(t, err, "Failed to decode")
   273  
   274  	// Verify the result
   275  	expectedValue := "value"
   276  	assert.Equal(t, expectedValue, result["test"], "Expected decoded result to be %s", expectedValue)
   277  }
   278  
   279  // TestSchemaReferenceOrdering tests that schema references work correctly regardless
   280  // of compilation order - parent schema can be compiled before referenced child schema
   281  func TestSchemaReferenceOrdering(t *testing.T) {
   282  	compiler := NewCompiler()
   283  
   284  	childSchema := []byte(`{
   285  		"$id": "http://example.com/child",
   286  		"type": "object",
   287  		"properties": {
   288  			"key": { "type": "string" }
   289  		}
   290  	}`)
   291  
   292  	parentSchema := []byte(`{
   293  		"type": "object",
   294  		"properties": {
   295  			"child": { "$ref": "http://example.com/child" }
   296  		}
   297  	}`)
   298  
   299  	// Compile parent first, then child - this should now work correctly
   300  	parentCompiledSchema, err := compiler.Compile(parentSchema)
   301  	require.NoError(t, err, "Failed to compile parent schema")
   302  
   303  	_, err = compiler.Compile(childSchema)
   304  	require.NoError(t, err, "Failed to compile child schema")
   305  
   306  	// Verify that reference is now resolved
   307  	require.NotNil(t, parentCompiledSchema.Properties, "Properties should not be nil")
   308  	childProp, exists := (*parentCompiledSchema.Properties)["child"]
   309  	require.True(t, exists, "child property should exist")
   310  	require.NotNil(t, childProp.ResolvedRef, "Reference should have been resolved after child schema compilation")
   311  
   312  	// Test valid data
   313  	validData := map[string]interface{}{
   314  		"child": map[string]interface{}{
   315  			"key": "valid",
   316  		},
   317  	}
   318  	result := parentCompiledSchema.Validate(validData)
   319  	assert.True(t, result.IsValid(), "Valid data should pass validation")
   320  
   321  	// Test invalid data - string instead of object
   322  	invalidData1 := map[string]interface{}{
   323  		"child": "string",
   324  	}
   325  	result = parentCompiledSchema.Validate(invalidData1)
   326  	assert.False(t, result.IsValid(), "Invalid data (string instead of object) should fail validation")
   327  
   328  	// Test invalid data - wrong type for key
   329  	invalidData2 := map[string]interface{}{
   330  		"child": map[string]interface{}{
   331  			"key": false,
   332  		},
   333  	}
   334  	result = parentCompiledSchema.Validate(invalidData2)
   335  	assert.False(t, result.IsValid(), "Invalid data (boolean instead of string) should fail validation")
   336  }
   337  
   338  // TestSchemaReferenceOrderingReversed tests the original working order for comparison
   339  func TestSchemaReferenceOrderingReversed(t *testing.T) {
   340  	compiler := NewCompiler()
   341  
   342  	childSchema := []byte(`{
   343  		"$id": "http://example.com/child",
   344  		"type": "object",
   345  		"properties": {
   346  			"key": { "type": "string" }
   347  		}
   348  	}`)
   349  
   350  	parentSchema := []byte(`{
   351  		"type": "object",
   352  		"properties": {
   353  			"child": { "$ref": "http://example.com/child" }
   354  		}
   355  	}`)
   356  
   357  	// Compile child first, then parent - this should work
   358  	_, err := compiler.Compile(childSchema)
   359  	require.NoError(t, err, "Failed to compile child schema")
   360  
   361  	parentCompiledSchema, err := compiler.Compile(parentSchema)
   362  	require.NoError(t, err, "Failed to compile parent schema")
   363  
   364  	// Test valid data
   365  	validData := map[string]interface{}{
   366  		"child": map[string]interface{}{
   367  			"key": "valid",
   368  		},
   369  	}
   370  	result := parentCompiledSchema.Validate(validData)
   371  	assert.True(t, result.IsValid(), "Valid data should pass validation")
   372  
   373  	// Test invalid data - string instead of object
   374  	invalidData1 := map[string]interface{}{
   375  		"child": "string",
   376  	}
   377  	result = parentCompiledSchema.Validate(invalidData1)
   378  	assert.False(t, result.IsValid(), "Invalid data (string instead of object) should fail validation")
   379  
   380  	// Test invalid data - wrong type for key
   381  	invalidData2 := map[string]interface{}{
   382  		"child": map[string]interface{}{
   383  			"key": false,
   384  		},
   385  	}
   386  	result = parentCompiledSchema.Validate(invalidData2)
   387  	assert.False(t, result.IsValid(), "Invalid data (boolean instead of string) should fail validation")
   388  }
   389  
   390  // TestCompileBatchWithCrossReferences tests that CompileBatch can handle schemas
   391  // with cross-references without causing nil pointer dereference errors
   392  // This test specifically addresses the fix for using s.GetCompiler() instead of s.compiler
   393  func TestCompileBatchWithCrossReferences(t *testing.T) {
   394  	compiler := NewCompiler()
   395  
   396  	// Define schemas with cross-references
   397  	schemas := map[string][]byte{
   398  		"person.json": []byte(`{
   399  			"$id": "person.json",
   400  			"type": "object",
   401  			"properties": {
   402  				"name": {"type": "string"},
   403  				"address": {"$ref": "address.json"},
   404  				"employer": {"$ref": "company.json"}
   405  			},
   406  			"required": ["name"]
   407  		}`),
   408  		"address.json": []byte(`{
   409  			"$id": "address.json",
   410  			"type": "object",
   411  			"properties": {
   412  				"street": {"type": "string"},
   413  				"city": {"type": "string"},
   414  				"country": {"$ref": "country.json"}
   415  			},
   416  			"required": ["street", "city"]
   417  		}`),
   418  		"company.json": []byte(`{
   419  			"$id": "company.json",
   420  			"type": "object",
   421  			"properties": {
   422  				"name": {"type": "string"},
   423  				"address": {"$ref": "address.json"}
   424  			},
   425  			"required": ["name"]
   426  		}`),
   427  		"country.json": []byte(`{
   428  			"$id": "country.json",
   429  			"type": "object",
   430  			"properties": {
   431  				"name": {"type": "string"},
   432  				"code": {"type": "string"}
   433  			},
   434  			"required": ["name", "code"]
   435  		}`),
   436  	}
   437  
   438  	// CompileBatch should not panic with cross-references
   439  	compiledSchemas, err := compiler.CompileBatch(schemas)
   440  	require.NoError(t, err, "CompileBatch should not fail with cross-references")
   441  	require.Len(t, compiledSchemas, 4, "All schemas should be compiled")
   442  
   443  	// Test that all schemas are properly compiled
   444  	for schemaID, schema := range compiledSchemas {
   445  		assert.NotNil(t, schema, "Schema %s should not be nil", schemaID)
   446  		assert.Equal(t, schemaID, schema.ID, "Schema ID should match: %s", schemaID)
   447  	}
   448  
   449  	// Test validation with the compiled schemas
   450  	personSchema := compiledSchemas["person.json"]
   451  	require.NotNil(t, personSchema, "Person schema should be available")
   452  
   453  	// Valid test data
   454  	validData := map[string]interface{}{
   455  		"name": "John Doe",
   456  		"address": map[string]interface{}{
   457  			"street": "123 Main St",
   458  			"city":   "Anytown",
   459  			"country": map[string]interface{}{
   460  				"name": "United States",
   461  				"code": "US",
   462  			},
   463  		},
   464  		"employer": map[string]interface{}{
   465  			"name": "Acme Corp",
   466  			"address": map[string]interface{}{
   467  				"street": "456 Business Ave",
   468  				"city":   "Corporate City",
   469  				"country": map[string]interface{}{
   470  					"name": "United States",
   471  					"code": "US",
   472  				},
   473  			},
   474  		},
   475  	}
   476  
   477  	result := personSchema.Validate(validData)
   478  	assert.True(t, result.IsValid(), "Valid data should pass validation")
   479  
   480  	// Invalid test data - missing required field
   481  	invalidData := map[string]interface{}{
   482  		"address": map[string]interface{}{
   483  			"street": "123 Main St",
   484  			"city":   "Anytown",
   485  		},
   486  	}
   487  
   488  	result = personSchema.Validate(invalidData)
   489  	assert.False(t, result.IsValid(), "Invalid data (missing required name) should fail validation")
   490  }
   491  
   492  // TestCompileBatchWithNestedReferences tests CompileBatch with deeply nested references
   493  // to ensure the fix for GetCompiler() works correctly in all contexts
   494  func TestCompileBatchWithNestedReferences(t *testing.T) {
   495  	compiler := NewCompiler()
   496  
   497  	schemas := map[string][]byte{
   498  		"root.json": []byte(`{
   499  			"$id": "root.json",
   500  			"type": "object",
   501  			"properties": {
   502  				"data": {
   503  					"type": "object",
   504  					"properties": {
   505  						"nested": {"$ref": "nested.json"}
   506  					}
   507  				}
   508  			}
   509  		}`),
   510  		"nested.json": []byte(`{
   511  			"$id": "nested.json",
   512  			"type": "object",
   513  			"properties": {
   514  				"deep": {
   515  					"type": "object",
   516  					"properties": {
   517  						"reference": {"$ref": "leaf.json"}
   518  					}
   519  				}
   520  			}
   521  		}`),
   522  		"leaf.json": []byte(`{
   523  			"$id": "leaf.json",
   524  			"type": "object",
   525  			"properties": {
   526  				"value": {"type": "string"}
   527  			},
   528  			"required": ["value"]
   529  		}`),
   530  	}
   531  
   532  	// This should not panic due to nil compiler references
   533  	compiledSchemas, err := compiler.CompileBatch(schemas)
   534  	require.NoError(t, err, "CompileBatch should handle nested references")
   535  	require.Len(t, compiledSchemas, 3, "All schemas should be compiled")
   536  
   537  	// Test validation works through the entire reference chain
   538  	rootSchema := compiledSchemas["root.json"]
   539  	testData := map[string]interface{}{
   540  		"data": map[string]interface{}{
   541  			"nested": map[string]interface{}{
   542  				"deep": map[string]interface{}{
   543  					"reference": map[string]interface{}{
   544  						"value": "test string",
   545  					},
   546  				},
   547  			},
   548  		},
   549  	}
   550  
   551  	result := rootSchema.Validate(testData)
   552  	assert.True(t, result.IsValid(), "Valid nested data should pass validation")
   553  }