...

Source file src/github.com/ory/x/jsonschemax/keys_test.go

Documentation: github.com/ory/x/jsonschemax

     1  package jsonschemax
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math/big"
     9  	"regexp"
    10  	"testing"
    11  
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/ory/jsonschema/v3"
    18  )
    19  
    20  const recursiveSchema = `{
    21    "$schema": "http://json-schema.org/draft-07/schema#",
    22    "$id": "test.json",
    23    "definitions": {
    24      "foo": {
    25        "type": "object",
    26        "properties": {
    27  		"bars": {
    28  			"type": "string",
    29  			"format": "email",
    30  			"pattern": ".*"
    31  		},
    32          "bar": {
    33            "$ref": "#/definitions/bar"
    34          }
    35        }
    36      },
    37      "bar": {
    38        "type": "object",
    39        "properties": {
    40  		"foos": {
    41  		  "type": "string",
    42  		  "minLength": 1,
    43  		  "maxLength": 10
    44  		},
    45          "foo": {
    46            "$ref": "#/definitions/foo"
    47          }
    48        }
    49      }
    50    },
    51    "type": "object",
    52    "properties": {
    53      "bar": {
    54        "$ref": "#/definitions/bar"
    55      }
    56    }
    57  }`
    58  
    59  func readFile(t *testing.T, path string) string {
    60  	schema, err := ioutil.ReadFile(path)
    61  	require.NoError(t, err)
    62  	return string(schema)
    63  }
    64  
    65  func assertEqualPaths(t *testing.T, expected byName, actual byName) {
    66  	for i := range expected {
    67  		t.Run("path="+expected[i].Name, func(t *testing.T) {
    68  			e := expected[i]
    69  			// because default if not given is -1
    70  			if e.MinLength == 0 {
    71  				e.MinLength = -1
    72  			}
    73  			if e.MaxLength == 0 {
    74  				e.MaxLength = -1
    75  			}
    76  
    77  			a := actual[i]
    78  			assert.Equal(t, e.Pattern, a.Pattern, fmt.Sprintf("path: %s\n", e.Name))
    79  
    80  			e.Pattern = nil
    81  			a.Pattern = nil
    82  
    83  			if e.Minimum != nil {
    84  				assert.NotNil(t, a.Minimum)
    85  				assert.Equal(t, e.Minimum.String(), e.Minimum.String(), fmt.Sprintf("path: %s\n", e.Name))
    86  			} else {
    87  				assert.Nil(t, a.Minimum)
    88  			}
    89  			if e.Maximum != nil {
    90  				assert.NotNil(t, a.Maximum)
    91  				assert.Equal(t, e.Maximum.String(), e.Maximum.String(), fmt.Sprintf("path: %s\n", e.Name))
    92  			} else {
    93  				assert.Nil(t, a.Maximum)
    94  			}
    95  
    96  			e.Minimum = nil
    97  			a.Minimum = nil
    98  			e.Maximum = nil
    99  			a.Maximum = nil
   100  
   101  			assert.Equal(t, e, a)
   102  		})
   103  	}
   104  }
   105  
   106  const fooExtensionName = "fooExtension"
   107  
   108  type (
   109  	extensionConfig struct {
   110  		NotAJSONSchemaKey string `json:"not-a-json-schema-key"`
   111  	}
   112  )
   113  
   114  func fooExtensionCompile(_ jsonschema.CompilerContext, m map[string]interface{}) (interface{}, error) {
   115  	if raw, ok := m[fooExtensionName]; ok {
   116  		var b bytes.Buffer
   117  		if err := json.NewEncoder(&b).Encode(raw); err != nil {
   118  			return nil, errors.WithStack(err)
   119  		}
   120  
   121  		var e extensionConfig
   122  		if err := json.NewDecoder(&b).Decode(&e); err != nil {
   123  			return nil, errors.WithStack(err)
   124  		}
   125  
   126  		return &e, nil
   127  	}
   128  	return nil, nil
   129  }
   130  
   131  func fooExtensionValidate(_ jsonschema.ValidationContext, _, _ interface{}) error {
   132  	return nil
   133  }
   134  
   135  func (ec *extensionConfig) EnhancePath(p Path) map[string]interface{} {
   136  	if ec.NotAJSONSchemaKey != "" {
   137  		fmt.Printf("enhancing path: %s with custom property %s\n", p.Name, ec.NotAJSONSchemaKey)
   138  		return map[string]interface{}{
   139  			ec.NotAJSONSchemaKey: p.Name,
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  func TestListPathsWithRecursion(t *testing.T) {
   146  	for k, tc := range []struct {
   147  		recursion uint8
   148  		expected  byName
   149  	}{
   150  		{
   151  			recursion: 5,
   152  			expected: byName{
   153  				Path{
   154  					Name:      "bar.foo.bar.foo.bar.foos",
   155  					Default:   interface{}(nil),
   156  					Type:      "",
   157  					TypeHint:  String,
   158  					MaxLength: 10,
   159  					MinLength: 1,
   160  				},
   161  				Path{
   162  					Name:      "bar.foo.bar.foo.bars",
   163  					Default:   interface{}(nil),
   164  					Type:      "",
   165  					Format:    "email",
   166  					TypeHint:  String,
   167  					Pattern:   regexp.MustCompile(".*"),
   168  					MaxLength: -1,
   169  					MinLength: -1,
   170  				},
   171  				Path{
   172  					Name:      "bar.foo.bar.foos",
   173  					Default:   interface{}(nil),
   174  					Type:      "",
   175  					TypeHint:  String,
   176  					MaxLength: 10,
   177  					MinLength: 1,
   178  				},
   179  				Path{
   180  					Name:      "bar.foo.bars",
   181  					Default:   interface{}(nil),
   182  					Type:      "",
   183  					TypeHint:  String,
   184  					Format:    "email",
   185  					Pattern:   regexp.MustCompile(".*"),
   186  					MaxLength: -1,
   187  					MinLength: -1,
   188  				},
   189  				Path{
   190  					Name:      "bar.foos",
   191  					Default:   interface{}(nil),
   192  					Type:      "",
   193  					TypeHint:  String,
   194  					MaxLength: 10,
   195  					MinLength: 1,
   196  				},
   197  			},
   198  		},
   199  	} {
   200  		t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
   201  			c := jsonschema.NewCompiler()
   202  			require.NoError(t, c.AddResource("test.json", bytes.NewBufferString(recursiveSchema)))
   203  			actual, err := ListPathsWithRecursion("test.json", c, tc.recursion)
   204  			require.NoError(t, err)
   205  			assertEqualPaths(t, tc.expected, actual)
   206  		})
   207  	}
   208  }
   209  
   210  func TestListPaths(t *testing.T) {
   211  	for k, tc := range []struct {
   212  		schema    string
   213  		expectErr bool
   214  		expected  byName
   215  		extension *jsonschema.Extension
   216  	}{
   217  		{
   218  			schema: readFile(t, "./stub/.oathkeeper.schema.json"),
   219  			expected: byName{
   220  				Path{Name: "access_rules.repositories", Type: []string{}, TypeHint: StringSlice},
   221  				Path{Name: "authenticators.anonymous.config.subject", Default: "anonymous", Type: "", TypeHint: String},
   222  				Path{Name: "authenticators.anonymous.enabled", Default: false, Type: false, TypeHint: Bool},
   223  				Path{Name: "authenticators.cookie_session.config.check_session_url", Type: "", TypeHint: String, Format: "uri"},
   224  				Path{Name: "authenticators.cookie_session.config.only", Type: []string{}, TypeHint: StringSlice},
   225  				Path{Name: "authenticators.cookie_session.enabled", Default: false, Type: false, TypeHint: Bool},
   226  				Path{Name: "authenticators.jwt.config.allowed_algorithms", Type: []string{}, TypeHint: StringSlice},
   227  				Path{Name: "authenticators.jwt.config.jwks_urls", Type: []string{}, TypeHint: StringSlice},
   228  				Path{Name: "authenticators.jwt.config.required_scope", Type: []string{}, TypeHint: StringSlice},
   229  				Path{Name: "authenticators.jwt.config.scope_strategy", Default: "none", Type: "", TypeHint: String, Enum: []interface{}{"hierarchic", "exact", "wildcard", "none"}},
   230  				Path{Name: "authenticators.jwt.config.target_audience", Type: []string{}, TypeHint: StringSlice},
   231  				Path{Name: "authenticators.jwt.config.token_from.header", Type: "", TypeHint: String},
   232  				Path{Name: "authenticators.jwt.config.token_from.query_parameter", Type: "", TypeHint: String},
   233  				Path{Name: "authenticators.jwt.config.trusted_issuers", Type: []string{}, TypeHint: StringSlice},
   234  				Path{Name: "authenticators.jwt.enabled", Default: false, Type: false, TypeHint: Bool},
   235  				Path{Name: "authenticators.noop.enabled", Default: false, Type: false, TypeHint: Bool},
   236  				Path{Name: "authenticators.oauth2_client_credentials.config.required_scope", Type: []string{}, TypeHint: StringSlice},
   237  				Path{Name: "authenticators.oauth2_client_credentials.config.token_url", Type: "", TypeHint: String, Format: "uri"},
   238  				Path{Name: "authenticators.oauth2_client_credentials.enabled", Default: false, Type: false, TypeHint: Bool},
   239  				Path{Name: "authenticators.oauth2_introspection.config.introspection_url", Type: "", TypeHint: String, Format: "uri"},
   240  				Path{Name: "authenticators.oauth2_introspection.config.pre_authorization.client_id", Type: "", TypeHint: String},
   241  				Path{Name: "authenticators.oauth2_introspection.config.pre_authorization.client_secret", Type: "", TypeHint: String},
   242  				Path{Name: "authenticators.oauth2_introspection.config.pre_authorization.enabled", Default: false, Type: false, TypeHint: Bool},
   243  				Path{Name: "authenticators.oauth2_introspection.config.pre_authorization.scope", Type: []string{}, TypeHint: StringSlice},
   244  				Path{Name: "authenticators.oauth2_introspection.config.pre_authorization.token_url", Type: "", TypeHint: String, Format: "uri"},
   245  				Path{Name: "authenticators.oauth2_introspection.config.required_scope", Type: []string{}, TypeHint: StringSlice},
   246  				Path{Name: "authenticators.oauth2_introspection.config.scope_strategy", Default: "none", Type: "", TypeHint: String, Enum: []interface{}{"hierarchic", "exact", "wildcard", "none"}},
   247  				Path{Name: "authenticators.oauth2_introspection.config.target_audience", Type: []string{}, TypeHint: StringSlice},
   248  				Path{Name: "authenticators.oauth2_introspection.config.token_from", Type: map[string]interface{}{}, TypeHint: JSON},
   249  				Path{Name: "authenticators.oauth2_introspection.config.token_from.header", Type: "", TypeHint: String},
   250  				Path{Name: "authenticators.oauth2_introspection.config.token_from.query_parameter", Type: "", TypeHint: String},
   251  				Path{Name: "authenticators.oauth2_introspection.config.trusted_issuers", Type: []string{}, TypeHint: StringSlice},
   252  				Path{Name: "authenticators.oauth2_introspection.enabled", Default: false, Type: false, TypeHint: Bool},
   253  				Path{Name: "authenticators.unauthorized.enabled", Default: false, Type: false, TypeHint: Bool},
   254  				Path{Name: "authorizers.allow.enabled", Default: false, Type: false, TypeHint: Bool},
   255  				Path{Name: "authorizers.deny.enabled", Default: false, Type: false, TypeHint: Bool},
   256  				Path{Name: "authorizers.keto_engine_acp_ory.config.base_url", Type: "", TypeHint: String, Format: "uri"},
   257  				Path{Name: "authorizers.keto_engine_acp_ory.config.flavor", Type: "", TypeHint: String},
   258  				Path{Name: "authorizers.keto_engine_acp_ory.config.required_action", Default: "unset", Type: "", TypeHint: String},
   259  				Path{Name: "authorizers.keto_engine_acp_ory.config.required_resource", Default: "unset", Type: "", TypeHint: String},
   260  				Path{Name: "authorizers.keto_engine_acp_ory.config.subject", Type: "", TypeHint: String},
   261  				Path{Name: "authorizers.keto_engine_acp_ory.enabled", Default: false, Type: false, TypeHint: Bool},
   262  				Path{Name: "log.format", Default: "text", Type: "", TypeHint: String, Enum: []interface{}{"text", "json"}},
   263  				Path{Name: "log.level", Default: "info", Type: "", TypeHint: String, Enum: []interface{}{"panic", "fatal", "error", "warn", "info", "debug"}},
   264  				Path{Name: "mutators.cookie.config.cookies", Type: map[string]interface{}{}, TypeHint: JSON},
   265  				Path{Name: "mutators.cookie.enabled", Default: false, Type: false, TypeHint: Bool},
   266  				Path{Name: "mutators.header.config.headers", Type: map[string]interface{}{}, TypeHint: JSON},
   267  				Path{Name: "mutators.header.enabled", Default: false, Type: false, TypeHint: Bool},
   268  				Path{Name: "mutators.hydrator.config.api.auth.basic.password", Type: "", TypeHint: String},
   269  				Path{Name: "mutators.hydrator.config.api.auth.basic.username", Type: "", TypeHint: String},
   270  				Path{Name: "mutators.hydrator.config.api.retry.delay_in_milliseconds", Default: float64(3), Type: float64(0), TypeHint: Int, Minimum: big.NewFloat(0)},
   271  				Path{Name: "mutators.hydrator.config.api.retry.number_of_retries", Default: float64(100), Type: float64(0), TypeHint: Float, Minimum: big.NewFloat(0)},
   272  				Path{Name: "mutators.hydrator.config.api.url", Type: "", TypeHint: String, Format: "uri"},
   273  				Path{Name: "mutators.hydrator.enabled", Default: false, Type: false, TypeHint: Bool},
   274  				Path{Name: "mutators.id_token.config.claims", Type: "", TypeHint: String},
   275  				Path{Name: "mutators.id_token.config.issuer_url", Type: "", TypeHint: String},
   276  				Path{Name: "mutators.id_token.config.jwks_url", Type: "", TypeHint: String, Format: "uri"},
   277  				Path{Name: "mutators.id_token.config.ttl", Default: "1m", Type: "", TypeHint: String, Pattern: regexp.MustCompile("^[0-9]+(ns|us|ms|s|m|h)$")},
   278  				Path{Name: "mutators.id_token.enabled", Default: false, Type: false, TypeHint: Bool},
   279  				Path{Name: "mutators.noop.enabled", Default: false, Type: false, TypeHint: Bool},
   280  				Path{Name: "profiling", Type: "", TypeHint: String, Enum: []interface{}{"cpu", "mem"}},
   281  				Path{Name: "serve.api.cors.allow_credentials", Default: false, Type: false, TypeHint: Bool},
   282  				Path{Name: "serve.api.cors.allowed_headers", Default: []interface{}{"Authorization", "Content-Type"}, MinLength: 1,
   283  					Type: []string{}, TypeHint: StringSlice},
   284  				Path{Name: "serve.api.cors.allowed_methods", Default: []interface{}{"GET", "POST", "PUT", "PATCH", "DELETE"},
   285  					Type: []string{}, TypeHint: StringSlice},
   286  				Path{Name: "serve.api.cors.allowed_origins", Default: []interface{}{"*"},
   287  					Type: []string{}, TypeHint: StringSlice},
   288  				Path{Name: "serve.api.cors.debug", Default: false, Type: false, TypeHint: Bool},
   289  				Path{Name: "serve.api.cors.enabled", Default: false, Type: false, TypeHint: Bool},
   290  				Path{Name: "serve.api.cors.exposed_headers", Default: []interface{}{"Content-Type"}, MinLength: 1,
   291  					Type: []string{}, TypeHint: StringSlice},
   292  				Path{Name: "serve.api.cors.max_age", Default: float64(0), Type: float64(0), TypeHint: Float},
   293  				Path{Name: "serve.api.host", Default: "", Type: "", TypeHint: String},
   294  				Path{Name: "serve.api.port", Default: float64(4456), Type: float64(0), TypeHint: Float},
   295  				Path{Name: "serve.api.tls.cert.base64", Type: "", TypeHint: String},
   296  				Path{Name: "serve.api.tls.cert.path", Type: "", TypeHint: String},
   297  				Path{Name: "serve.api.tls.key.base64", Type: "", TypeHint: String},
   298  				Path{Name: "serve.api.tls.key.path", Type: "", TypeHint: String},
   299  				Path{Name: "serve.proxy.cors.allow_credentials", Default: false, Type: false, TypeHint: Bool},
   300  				Path{Name: "serve.proxy.cors.allowed_headers", Default: []interface{}{"Authorization", "Content-Type"}, MinLength: 1,
   301  					Type: []string{}, TypeHint: StringSlice},
   302  				Path{Name: "serve.proxy.cors.allowed_methods", Default: []interface{}{"GET", "POST", "PUT", "PATCH", "DELETE"},
   303  					Type: []string{}, TypeHint: StringSlice},
   304  				Path{Name: "serve.proxy.cors.allowed_origins", Default: []interface{}{"*"},
   305  					Type: []string{}, TypeHint: StringSlice},
   306  				Path{Name: "serve.proxy.cors.debug", Default: false, Type: false, TypeHint: Bool},
   307  				Path{Name: "serve.proxy.cors.enabled", Default: false, Type: false, TypeHint: Bool},
   308  				Path{Name: "serve.proxy.cors.exposed_headers", Default: []interface{}{"Content-Type"}, MinLength: 1,
   309  					Type: []string{}, TypeHint: StringSlice},
   310  				Path{Name: "serve.proxy.cors.max_age", Default: float64(0), Type: float64(0), TypeHint: Float},
   311  				Path{Name: "serve.proxy.host", Default: "", Type: "", TypeHint: String},
   312  				Path{Name: "serve.proxy.port", Default: float64(4455), Type: float64(0), TypeHint: Float},
   313  				Path{Name: "serve.proxy.timeout.idle", Default: "120s", Type: "", TypeHint: String, Pattern: regexp.MustCompile("^[0-9]+(ns|us|ms|s|m|h)$")},
   314  				Path{Name: "serve.proxy.timeout.read", Default: "5s", Type: "", TypeHint: String, Pattern: regexp.MustCompile("^[0-9]+(ns|us|ms|s|m|h)$")},
   315  				Path{Name: "serve.proxy.timeout.write", Default: "120s", Type: "", TypeHint: String, Pattern: regexp.MustCompile("^[0-9]+(ns|us|ms|s|m|h)$")},
   316  				Path{Name: "serve.proxy.tls.cert.base64", Type: "", TypeHint: String},
   317  				Path{Name: "serve.proxy.tls.cert.path", Type: "", TypeHint: String},
   318  				Path{Name: "serve.proxy.tls.key.base64", Type: "", TypeHint: String},
   319  				Path{Name: "serve.proxy.tls.key.path", Type: "", TypeHint: String},
   320  			},
   321  		},
   322  		{
   323  			schema: readFile(t, "./stub/config.schema.json"),
   324  			expected: []Path{
   325  				{
   326  					Name:     "dsn",
   327  					Default:  nil,
   328  					TypeHint: String,
   329  					Type:     "",
   330  				},
   331  			},
   332  		},
   333  		{
   334  			// this should fail because of recursion
   335  			schema:    recursiveSchema,
   336  			expectErr: true,
   337  		},
   338  		{
   339  			schema: `{
   340    "$schema": "http://json-schema.org/draft-07/schema#",
   341    "$id": "test.json",
   342    "oneOf": [
   343      {
   344        "type": "object",
   345        "properties": {
   346          "list": {
   347            "type": "array",
   348            "items": {
   349              "type": "string"
   350            }
   351          },
   352          "foo": {
   353            "default": false,
   354            "type": "boolean"
   355          },
   356          "bar": {
   357            "type": "boolean",
   358            "default": "asdf",
   359            "readOnly": true
   360          }
   361        }
   362      },
   363      {
   364        "type": "object",
   365        "properties": {
   366          "foo": {
   367            "type": "boolean"
   368          }
   369        }
   370      }
   371    ]
   372  }`,
   373  			expected: byName{
   374  				{
   375  					Name:     "bar",
   376  					Default:  "asdf",
   377  					Type:     false,
   378  					TypeHint: Bool,
   379  					ReadOnly: true,
   380  				},
   381  				{
   382  					Name:     "foo",
   383  					Default:  false,
   384  					Type:     false,
   385  					TypeHint: Bool,
   386  				},
   387  				{
   388  					Name:     "list",
   389  					Type:     []string{},
   390  					TypeHint: StringSlice,
   391  				},
   392  			},
   393  		},
   394  		{
   395  			schema: `{
   396    "$schema": "http://json-schema.org/draft-07/schema#",
   397    "$id": "test.json",
   398    "type": "object",
   399    "properties": {
   400      "foo": {
   401        "type": "boolean"
   402      },
   403      "bar": {
   404        "type": "string",
   405        "fooExtension": {
   406          "not-a-json-schema-key": "foobar"
   407        }
   408      }
   409    }
   410  }`,
   411  			extension: &jsonschema.Extension{
   412  				Meta:     nil,
   413  				Compile:  fooExtensionCompile,
   414  				Validate: fooExtensionValidate,
   415  			},
   416  			expected: byName{
   417  				{
   418  					Name:     "bar",
   419  					Type:     "",
   420  					TypeHint: String,
   421  					CustomProperties: map[string]interface{}{
   422  						"foobar": "bar",
   423  					},
   424  				},
   425  				{
   426  					Name:     "foo",
   427  					Type:     false,
   428  					TypeHint: Bool,
   429  				},
   430  			},
   431  		},
   432  	} {
   433  		t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
   434  			c := jsonschema.NewCompiler()
   435  			if tc.extension != nil {
   436  				c.Extensions[fooExtensionName] = *tc.extension
   437  			}
   438  
   439  			require.NoError(t, c.AddResource("test.json", bytes.NewBufferString(tc.schema)))
   440  			actual, err := ListPaths("test.json", c)
   441  			if tc.expectErr {
   442  				require.Error(t, err, "%+v", actual)
   443  				return
   444  			}
   445  			require.NoError(t, err)
   446  			assertEqualPaths(t, tc.expected, actual)
   447  		})
   448  	}
   449  }
   450  

View as plain text