-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from CrowdStrike/chore/add_schema_helpers
chore: add fdktest with schema helper fns
- Loading branch information
Showing
7 changed files
with
349 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package fdktest | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/xeipuuv/gojsonschema" | ||
|
||
fdk "github.com/CrowdStrike/foundry-fn-go" | ||
) | ||
|
||
// SchemaOK validates that the provided schema conforms to JSON Schema. | ||
func SchemaOK(schema string) error { | ||
schemaLoader := gojsonschema.NewSchemaLoader() | ||
_, err := schemaLoader.Compile(gojsonschema.NewStringLoader(schema)) | ||
return err | ||
} | ||
|
||
// HandlerSchemaOK validates the handler and schema integrations. | ||
func HandlerSchemaOK(h fdk.Handler, r fdk.Request, reqSchema, respSchema string) error { | ||
if reqSchema != "" { | ||
if err := validateSchema(reqSchema, r.Body); err != nil { | ||
return fmt.Errorf("failed request schema validation: %w", err) | ||
} | ||
} | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
|
||
resp := h.Handle(ctx, r) | ||
|
||
if respSchema != "" { | ||
b, err := resp.Body.MarshalJSON() | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal response payload: %w", err) | ||
} | ||
|
||
err = validateSchema(respSchema, b) | ||
if err != nil { | ||
return fmt.Errorf("failed response schema validation: %w", err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func validateSchema(schema string, payload []byte) error { | ||
result, err := gojsonschema.Validate( | ||
gojsonschema.NewStringLoader(schema), | ||
gojsonschema.NewBytesLoader(payload), | ||
) | ||
if err != nil { | ||
return fmt.Errorf("failed to validate document against schema: %w", err) | ||
} | ||
|
||
var errs []error | ||
for _, resErr := range result.Errors() { | ||
errMsg := resErr.String() | ||
|
||
// sometimes the library prefixes the string message with (root): which is confusing and is best to defer | ||
// to the description message in this case | ||
id := resErr.Field() | ||
if len(resErr.Details()) > 0 && resErr.Details()["property"] != nil { | ||
id = fmt.Sprintf("%s.%s", id, resErr.Details()["property"]) | ||
} | ||
if resErr.Field() == "(root)" { | ||
errMsg = resErr.Description() | ||
if prop, ok := resErr.Details()["property"]; ok { | ||
id = prop.(string) | ||
} | ||
} | ||
errs = append(errs, errors.New(errMsg)) | ||
} | ||
|
||
return errors.Join(errs...) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
package fdktest_test | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
fdk "github.com/CrowdStrike/foundry-fn-go" | ||
"github.com/CrowdStrike/foundry-fn-go/fdktest" | ||
) | ||
|
||
func TestHandlerSchemaOK(t *testing.T) { | ||
type ( | ||
inputs struct { | ||
handler fdk.Handler | ||
req fdk.Request | ||
reqSchema string | ||
respSchema string | ||
} | ||
|
||
wantFn func(t *testing.T, err error) | ||
) | ||
|
||
tests := []struct { | ||
name string | ||
input inputs | ||
want wantFn | ||
}{ | ||
{ | ||
name: "with valid req and resp schema and compliant req and resp bodies should pass", | ||
input: inputs{ | ||
handler: fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response { | ||
return fdk.Response{Body: fdk.JSON(map[string]string{"foo": "bar"})} | ||
}), | ||
req: fdk.Request{ | ||
URL: "/", | ||
Method: http.MethodPost, | ||
Body: json.RawMessage(`{"postalCode": "55755"}`), | ||
}, | ||
reqSchema: `{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"type": "object", | ||
"properties": { | ||
"postalCode": { | ||
"type": "string", | ||
"description": "The person's first name.", | ||
"pattern": "\\d{5}" | ||
}, | ||
"optional": { | ||
"type": "string", | ||
"description": "The person's last name." | ||
} | ||
}, | ||
"required": [ | ||
"postalCode" | ||
] | ||
}`, | ||
respSchema: `{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"type": "object", | ||
"properties": { | ||
"foo": { | ||
"type": "string", | ||
"description": "The person's first name.", | ||
"enum": ["bar"] | ||
} | ||
}, | ||
"required": [ | ||
"foo" | ||
] | ||
}`, | ||
}, | ||
want: mustNoErr, | ||
}, | ||
{ | ||
name: "with valid req schema and invalid request body should fail", | ||
input: inputs{ | ||
handler: fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response { | ||
return fdk.Response{Body: fdk.JSON(map[string]string{"foo": "bar"})} | ||
}), | ||
req: fdk.Request{ | ||
URL: "/", | ||
Method: http.MethodPost, | ||
Body: json.RawMessage(`{"postalCode": "5"}`), | ||
}, | ||
reqSchema: `{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"type": "object", | ||
"properties": { | ||
"postalCode": { | ||
"type": "string", | ||
"description": "The person's first name.", | ||
"pattern": "\\d{5}" | ||
} | ||
}, | ||
"required": [ | ||
"postalCode" | ||
] | ||
}`, | ||
}, | ||
want: func(t *testing.T, err error) { | ||
errMsg := "failed request schema validation: postalCode: Does not match pattern '\\d{5}'" | ||
if err == nil || !strings.HasSuffix(err.Error(), errMsg) { | ||
t.Fatal("did not get expected error: ", err) | ||
} | ||
}, | ||
}, | ||
{ | ||
name: "with valid resp schema and invalid response body should fail", | ||
input: inputs{ | ||
handler: fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response { | ||
return fdk.Response{Body: fdk.JSON(map[string]string{"foo": "NOT BAR"})} | ||
}), | ||
req: fdk.Request{ | ||
URL: "/", | ||
Method: http.MethodPost, | ||
}, | ||
respSchema: `{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"type": "object", | ||
"properties": { | ||
"foo": { | ||
"type": "string", | ||
"description": "The person's first name.", | ||
"enum": ["bar"] | ||
} | ||
}, | ||
"required": [ | ||
"foo" | ||
] | ||
}`, | ||
}, | ||
want: func(t *testing.T, err error) { | ||
errMsg := "failed response schema validation: foo: foo must be one of the following: \"bar\"" | ||
if err == nil || !strings.HasSuffix(err.Error(), errMsg) { | ||
t.Fatal("did not get expected error: ", err) | ||
} | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
err := fdktest.HandlerSchemaOK(tt.input.handler, tt.input.req, tt.input.reqSchema, tt.input.respSchema) | ||
tt.want(t, err) | ||
}) | ||
} | ||
} | ||
|
||
func TestSchemaOK(t *testing.T) { | ||
schema := `{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"type": "object", | ||
"properties": { | ||
"postalCode": { | ||
"type": "string", | ||
"description": "The person's first name.", | ||
"pattern": "\\d{5}" | ||
}, | ||
"optional": { | ||
"type": "string", | ||
"description": "The person's last name." | ||
} | ||
}, | ||
"required": [ | ||
"postalCode" | ||
] | ||
}` | ||
|
||
t.Run("with valid schema should pass", func(t *testing.T) { | ||
err := fdktest.SchemaOK(schema) | ||
mustNoErr(t, err) | ||
}) | ||
|
||
t.Run("with invalid shcema should fail", func(t *testing.T) { | ||
invalidScheam := schema[15:] | ||
err := fdktest.SchemaOK(invalidScheam) | ||
if err == nil { | ||
t.Fatal("expected validation error") | ||
} | ||
}) | ||
} | ||
|
||
func mustNoErr(t *testing.T, err error) { | ||
if err != nil { | ||
t.Fatal("unexpected error: " + err.Error()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.