mirror of
				https://github.com/edgelesssys/constellation.git
				synced 2025-10-31 03:39:04 -04:00 
			
		
		
		
	 744a605602
			
		
	
	
		744a605602
		
			
		
	
	
	
	
		
			
			* re-use `ReadFromFile` in `CreateOrRead` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip]: add constraints Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] error formatting Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * wip Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * formatted error messages Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * state file validation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * allow overriding the constraints Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * dont validate on read Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add pre-create constraints Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * finish pre-init validation test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * finish post-init validation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use state file validation in CLI Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix apply tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Update internal/validation/errors.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * use transformator for tests * tidy * use empty check directly Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * conditional validation per CSP Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix rebase Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add default case Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * validate state-file as last input Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
		
			
				
	
	
		
			326 lines
		
	
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright (c) Edgeless Systems GmbH
 | |
| 
 | |
| SPDX-License-Identifier: AGPL-3.0-only
 | |
| */
 | |
| 
 | |
| package validation
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| var validDoc = func() *exampleDoc {
 | |
| 	return &exampleDoc{
 | |
| 		StrField: "abc",
 | |
| 		NumField: 42,
 | |
| 		MapField: &map[string]string{
 | |
| 			"empty": "",
 | |
| 		},
 | |
| 		NotEmptyField:   "certainly not.",
 | |
| 		MatchRegexField: "abc",
 | |
| 		OneOfField:      "one",
 | |
| 		OrLeftField:     "left",
 | |
| 		OrRightField:    "right",
 | |
| 		AndLeftField:    "left",
 | |
| 		AndRightField:   "right",
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidate(t *testing.T) {
 | |
| 	testCases := map[string]struct {
 | |
| 		doc          func() *exampleDoc
 | |
| 		opts         ValidateOptions
 | |
| 		wantErr      bool
 | |
| 		errAssertion func(*assert.Assertions, error) bool
 | |
| 	}{
 | |
| 		"valid": {
 | |
| 			doc:  validDoc,
 | |
| 			opts: ValidateOptions{},
 | |
| 		},
 | |
| 		"strField is not abc": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.StrField = "def"
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.strField: def must be abc")
 | |
| 			},
 | |
| 			opts: ValidateOptions{},
 | |
| 		},
 | |
| 		"numField is not 42": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.NumField = 43
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.numField: 43 must be equal to 42")
 | |
| 			},
 | |
| 		},
 | |
| 		"multiple errors": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.StrField = "def"
 | |
| 				doc.NumField = 43
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.strField: def must be abc") &&
 | |
| 					assert.Contains(err.Error(), "validating exampleDoc.numField: 43 must be equal to 42")
 | |
| 			},
 | |
| 			opts: ValidateOptions{},
 | |
| 		},
 | |
| 		"multiple errors, fail fast": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.StrField = "def"
 | |
| 				doc.NumField = 43
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.strField: def must be abc")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 		"map field is not empty": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.MapField = &map[string]string{
 | |
| 					"empty": "haha!",
 | |
| 				}
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.mapField[\"empty\"]: haha! must be empty")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 		"not empty field is empty": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.NotEmptyField = ""
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.notEmptyField: must not be empty")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 		"regex doesnt match": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.MatchRegexField = "dontmatch"
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.matchRegexField: dontmatch must match the pattern ^a.c$")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 		"field is not in 'oneof' values": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.OneOfField = "not in oneof"
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "validating exampleDoc.oneOfField: not in oneof must be one of [one two three]")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 		"'or' violated": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.OrLeftField = "not left"
 | |
| 				doc.OrRightField = "not right"
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "at least one of the constraints must be satisfied:") &&
 | |
| 					assert.Contains(err.Error(), "validating exampleDoc.orLeftField: not left must be equal to left") &&
 | |
| 					assert.Contains(err.Error(), "validating exampleDoc.orRightField: not right must be equal to right")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 		"'and' violated": {
 | |
| 			doc: func() *exampleDoc {
 | |
| 				doc := validDoc()
 | |
| 				doc.AndRightField = "not right"
 | |
| 				return doc
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			errAssertion: func(assert *assert.Assertions, err error) bool {
 | |
| 				return assert.Contains(err.Error(), "all of the constraints must be satisfied:") &&
 | |
| 					assert.Contains(err.Error(), "validating exampleDoc.andRightField: not right must be equal to right")
 | |
| 			},
 | |
| 			opts: ValidateOptions{
 | |
| 				ErrStrategy: FailFast,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for name, tc := range testCases {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			assert := assert.New(t)
 | |
| 			require := require.New(t)
 | |
| 
 | |
| 			err := NewValidator().Validate(tc.doc(), tc.opts)
 | |
| 			if tc.wantErr {
 | |
| 				require.Error(err)
 | |
| 				if !tc.errAssertion(assert, err) {
 | |
| 					t.Fatalf("unexpected error: %v", err)
 | |
| 				}
 | |
| 			} else {
 | |
| 				require.NoError(err)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type exampleDoc struct {
 | |
| 	StrField        string             `json:"strField"`
 | |
| 	NumField        int                `json:"numField"`
 | |
| 	MapField        *map[string]string `json:"mapField"`
 | |
| 	NotEmptyField   string             `json:"notEmptyField"`
 | |
| 	MatchRegexField string             `json:"matchRegexField"`
 | |
| 	OneOfField      string             `json:"oneOfField"`
 | |
| 	OrLeftField     string             `json:"orLeftField"`
 | |
| 	OrRightField    string             `json:"orRightField"`
 | |
| 	AndLeftField    string             `json:"andLeftField"`
 | |
| 	AndRightField   string             `json:"andRightField"`
 | |
| }
 | |
| 
 | |
| // Constraints implements the Validatable interface.
 | |
| func (d *exampleDoc) Constraints() []*Constraint {
 | |
| 	mapField := *(d.MapField)
 | |
| 
 | |
| 	return []*Constraint{
 | |
| 		d.strFieldNeedsToBeAbc().
 | |
| 			WithFieldTrace(d, &d.StrField),
 | |
| 		Equal(d.NumField, 42).
 | |
| 			WithFieldTrace(d, &d.NumField),
 | |
| 		Empty(mapField["empty"]).
 | |
| 			WithMapFieldTrace(d, d.MapField, "empty"),
 | |
| 		NotEmpty(d.NotEmptyField).
 | |
| 			WithFieldTrace(d, &d.NotEmptyField),
 | |
| 		MatchRegex(d.MatchRegexField, "^a.c$").
 | |
| 			WithFieldTrace(d, &d.MatchRegexField),
 | |
| 		OneOf(d.OneOfField, []string{"one", "two", "three"}).
 | |
| 			WithFieldTrace(d, &d.OneOfField),
 | |
| 		Or(
 | |
| 			Equal(d.OrLeftField, "left").
 | |
| 				WithFieldTrace(d, &d.OrLeftField),
 | |
| 			Equal(d.OrRightField, "right").
 | |
| 				WithFieldTrace(d, &d.OrRightField),
 | |
| 		),
 | |
| 		And(
 | |
| 			EvaluateAll,
 | |
| 			Equal(d.AndLeftField, "left").
 | |
| 				WithFieldTrace(d, &d.AndLeftField),
 | |
| 			Equal(d.AndRightField, "right").
 | |
| 				WithFieldTrace(d, &d.AndRightField),
 | |
| 		),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // StrFieldNeedsToBeAbc is an example for a custom constraint.
 | |
| func (d *exampleDoc) strFieldNeedsToBeAbc() *Constraint {
 | |
| 	return &Constraint{
 | |
| 		Satisfied: func() *TreeError {
 | |
| 			if d.StrField != "abc" {
 | |
| 				return NewErrorTree(
 | |
| 					fmt.Errorf("%s must be abc", d.StrField),
 | |
| 				)
 | |
| 			}
 | |
| 			return nil
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestOverrideConstraints(t *testing.T) {
 | |
| 	overrideConstraints := func(t *testing.T, wantCalled bool) func() []*Constraint {
 | |
| 		return func() []*Constraint {
 | |
| 			if !wantCalled {
 | |
| 				t.Fatal("overrideConstraints should not be called")
 | |
| 			}
 | |
| 			return []*Constraint{}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	testCases := map[string]struct {
 | |
| 		doc                exampleDocToOverride
 | |
| 		overrideFunc       func() []*Constraint
 | |
| 		wantOverrideCalled bool
 | |
| 		wantErr            bool
 | |
| 	}{
 | |
| 		"override constraints": {
 | |
| 			doc:                exampleDocToOverride{},
 | |
| 			overrideFunc:       overrideConstraints(t, true),
 | |
| 			wantOverrideCalled: true,
 | |
| 		},
 | |
| 		"do not override constraints": {
 | |
| 			doc: exampleDocToOverride{
 | |
| 				calledDocConstraints: true,
 | |
| 			},
 | |
| 			overrideFunc:       nil,
 | |
| 			wantOverrideCalled: false,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for name, tc := range testCases {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			assert := assert.New(t)
 | |
| 			require := require.New(t)
 | |
| 
 | |
| 			validator := NewValidator()
 | |
| 			err := validator.Validate(&tc.doc, ValidateOptions{
 | |
| 				OverrideConstraints: tc.overrideFunc,
 | |
| 			})
 | |
| 
 | |
| 			if tc.wantErr {
 | |
| 				require.Error(err)
 | |
| 			} else {
 | |
| 				require.NoError(err)
 | |
| 				if tc.wantOverrideCalled {
 | |
| 					assert.Equal(tc.doc.calledDocConstraints, false)
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type exampleDocToOverride struct {
 | |
| 	calledDocConstraints bool
 | |
| }
 | |
| 
 | |
| func (d *exampleDocToOverride) Constraints() []*Constraint {
 | |
| 	d.calledDocConstraints = true
 | |
| 	return []*Constraint{}
 | |
| }
 |