190 lines
7.6 KiB
Go
190 lines
7.6 KiB
Go
|
|
package customschema_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
|
||
|
|
"queryorchestration/internal/customschema"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestValidateSchemaDefinition exercises every rule listed in Plan
|
||
|
|
// Section 6.2 / Tracking Section 1.2 against the SchemaValidator. Every
|
||
|
|
// reject-case subtest is expected to FAIL against the current stub
|
||
|
|
// (which returns nil unconditionally) and PASS once the real
|
||
|
|
// implementation lands. Every accept-case subtest must pass against
|
||
|
|
// both the stub and the real implementation.
|
||
|
|
func TestValidateSchemaDefinition(t *testing.T) {
|
||
|
|
v := &customschema.SchemaValidator{}
|
||
|
|
|
||
|
|
// -------------------------------------------------------------------
|
||
|
|
// Accept-case subtests. These will "pass" against the stub and must
|
||
|
|
// remain green under the real implementation.
|
||
|
|
// -------------------------------------------------------------------
|
||
|
|
|
||
|
|
t.Run("valid_minimal_object_accepted", func(t *testing.T) {
|
||
|
|
schema := json.RawMessage(`{"type":"object","additionalProperties":false}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.NoError(t, err, "minimal valid object schema must be accepted")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("valid_object_with_properties_accepted", func(t *testing.T) {
|
||
|
|
schema := json.RawMessage(`{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","maxLength":100},"count":{"type":"integer","minimum":0}}}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.NoError(t, err, "valid object schema with typed properties must be accepted")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("additional_properties_true_accepted", func(t *testing.T) {
|
||
|
|
// The rule is that additionalProperties must be *present*, not that
|
||
|
|
// it must be false. Explicit `true` must also be accepted.
|
||
|
|
schema := json.RawMessage(`{"type":"object","additionalProperties":true}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.NoError(t, err, "explicit additionalProperties=true must be accepted")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("at_max_size_accepted", func(t *testing.T) {
|
||
|
|
// Construct a schema whose serialized length is exactly
|
||
|
|
// MaxSchemaDefinitionBytes. This proves the size rule is strict
|
||
|
|
// greater-than, not >=.
|
||
|
|
//
|
||
|
|
// Fixed scaffold (63 bytes):
|
||
|
|
// {"type":"object","additionalProperties":false,"description":""}
|
||
|
|
// Padding lives inside the description string.
|
||
|
|
const scaffoldLen = 63
|
||
|
|
padLen := customschema.MaxSchemaDefinitionBytes - scaffoldLen
|
||
|
|
pad := strings.Repeat("a", padLen)
|
||
|
|
raw := `{"type":"object","additionalProperties":false,"description":"` + pad + `"}`
|
||
|
|
require.Equal(t, customschema.MaxSchemaDefinitionBytes, len(raw),
|
||
|
|
"test setup: payload must be exactly MaxSchemaDefinitionBytes bytes")
|
||
|
|
|
||
|
|
schema := json.RawMessage(raw)
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.NoError(t, err, "schema exactly at MaxSchemaDefinitionBytes must be accepted")
|
||
|
|
})
|
||
|
|
|
||
|
|
// -------------------------------------------------------------------
|
||
|
|
// Reject-case subtests. These MUST FAIL against the stub (which
|
||
|
|
// returns nil) and MUST PASS against the real implementation.
|
||
|
|
// Expected-substring contract is documented inline so the golang
|
||
|
|
// engineer knows what error text to emit.
|
||
|
|
// -------------------------------------------------------------------
|
||
|
|
|
||
|
|
t.Run("invalid_json_rejected", func(t *testing.T) {
|
||
|
|
// Contract: error message must mention "invalid" (the golang
|
||
|
|
// engineer should wrap the json.Unmarshal error with a prefix
|
||
|
|
// containing the word "invalid", e.g. "invalid JSON: ...").
|
||
|
|
schema := json.RawMessage(`{this is not json`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "malformed JSON must be rejected")
|
||
|
|
require.Contains(t, strings.ToLower(err.Error()), "invalid",
|
||
|
|
"error message should mention 'invalid'")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("meta_validation_failure_rejected", func(t *testing.T) {
|
||
|
|
// Contract: error message must mention "type". The santhosh-tekuri
|
||
|
|
// library reports meta-validation failures against the draft
|
||
|
|
// 2020-12 meta-schema, and "type: nonsense" fails the enum check
|
||
|
|
// on the "type" keyword -- the word "type" is the most
|
||
|
|
// discriminating substring the golang engineer can reliably
|
||
|
|
// surface from the library's error output.
|
||
|
|
schema := json.RawMessage(`{"type":"nonsense"}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "schema failing JSON Schema meta-validation must be rejected")
|
||
|
|
require.Contains(t, strings.ToLower(err.Error()), "type",
|
||
|
|
"error message should mention 'type'")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("root_type_not_object_rejected", func(t *testing.T) {
|
||
|
|
// Contract: error message must mention "object" (Plan 6.2 rule 3
|
||
|
|
// is explicit that root type must be "object").
|
||
|
|
schema := json.RawMessage(`{"type":"array","items":{"type":"string"}}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "non-object root type must be rejected")
|
||
|
|
require.Contains(t, strings.ToLower(err.Error()), "object",
|
||
|
|
"error message should mention 'object'")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("root_type_missing_rejected", func(t *testing.T) {
|
||
|
|
// Contract: error message must mention "type". Omitted root
|
||
|
|
// `type` fails Plan 6.2 rule 3 (root type must equal "object")
|
||
|
|
// regardless of JSON Schema's own permissiveness.
|
||
|
|
schema := json.RawMessage(`{"additionalProperties":false}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "missing root type must be rejected")
|
||
|
|
require.Contains(t, strings.ToLower(err.Error()), "type",
|
||
|
|
"error message should mention 'type'")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("additional_properties_sub_schema_rejected", func(t *testing.T) {
|
||
|
|
// Contract: only boolean true/false is accepted. A sub-schema object
|
||
|
|
// like {"type":"string"} must be rejected with an error mentioning
|
||
|
|
// "additionalProperties". Plan Section 6.2 rule 4 is explicitly binary.
|
||
|
|
schema := json.RawMessage(`{"type":"object","additionalProperties":{"type":"string"}}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "sub-schema additionalProperties must be rejected")
|
||
|
|
require.Contains(t, err.Error(), "additionalProperties",
|
||
|
|
"error message should mention 'additionalProperties'")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("additional_properties_missing_rejected", func(t *testing.T) {
|
||
|
|
// Contract: error message must mention "additionalProperties"
|
||
|
|
// (Plan 6.2 rule 4 is explicit that this key must be present).
|
||
|
|
schema := json.RawMessage(`{"type":"object","properties":{"foo":{"type":"string"}}}`)
|
||
|
|
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "missing root additionalProperties must be rejected")
|
||
|
|
require.Contains(t, err.Error(), "additionalProperties",
|
||
|
|
"error message should mention 'additionalProperties'")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("oversize_rejected", func(t *testing.T) {
|
||
|
|
// Contract: error message must mention "size" (Plan 6.2 rule 5:
|
||
|
|
// Size must be <= MaxSchemaDefinitionBytes).
|
||
|
|
const scaffoldLen = 63
|
||
|
|
// One byte past the limit.
|
||
|
|
padLen := customschema.MaxSchemaDefinitionBytes - scaffoldLen + 1
|
||
|
|
pad := strings.Repeat("a", padLen)
|
||
|
|
raw := `{"type":"object","additionalProperties":false,"description":"` + pad + `"}`
|
||
|
|
require.Equal(t, customschema.MaxSchemaDefinitionBytes+1, len(raw),
|
||
|
|
"test setup: payload must be exactly MaxSchemaDefinitionBytes+1 bytes")
|
||
|
|
|
||
|
|
schema := json.RawMessage(raw)
|
||
|
|
err := v.ValidateSchemaDefinition(schema)
|
||
|
|
t.Logf("stub err = %v", err)
|
||
|
|
|
||
|
|
require.Error(t, err, "schema larger than MaxSchemaDefinitionBytes must be rejected")
|
||
|
|
require.Contains(t, strings.ToLower(err.Error()), "size",
|
||
|
|
"error message should mention 'size'")
|
||
|
|
})
|
||
|
|
}
|