Repository: getkin/kin-openapi
Pull request #906 (2024-02-09 21:43:56)

Make Loader testable
Make Loader testable by allowing it to load a spec from io.Reader.



Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 8b146f0..7418add 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -586,6 +586,9 @@ func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T,
 func (loader *Loader) LoadFromFile(location string) (*T, error)
     LoadFromFile loads a spec from a local file path
 
+func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error)
+    LoadFromStdin loads a spec from io.Reader
+
 func (loader *Loader) LoadFromStdin() (*T, error)
     LoadFromStdin loads a spec from stdin
 
diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt
index f0ba156..b05679e 100644
--- a/.github/docs/openapi3filter.txt
+++ b/.github/docs/openapi3filter.txt
@@ -331,6 +331,8 @@ type SecurityRequirementsError struct {
 
 func (err *SecurityRequirementsError) Error() string
 
+func (err SecurityRequirementsError) Unwrap() []error
+
 type StatusCoder interface {
 	StatusCode() int
 }
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 1128aaa..88ab566 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -126,9 +126,18 @@ func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
 
 // LoadFromStdin loads a spec from stdin
 func (loader *Loader) LoadFromStdin() (*T, error) {
-	data, err := io.ReadAll(os.Stdin)
+	return loader.LoadFromIoReader(os.Stdin)
+}
+
+// LoadFromStdin loads a spec from io.Reader
+func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) {
+	if reader == nil {
+		return nil, fmt.Errorf("invalid reader: %v", reader)
+	}
+
+	data, err := io.ReadAll(reader)
 	if err != nil {
-		return nil, fmt.Errorf("read from stdin: %w", err)
+		return nil, err
 	}
 	return loader.LoadFromData(data)
 }
diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go
index e78e982..ea7c7c3 100644
--- a/openapi3filter/errors.go
+++ b/openapi3filter/errors.go
@@ -89,3 +89,9 @@ func (err *SecurityRequirementsError) Error() string {
 
 	return buff.String()
 }
+
+var _ interface{ Unwrap() []error } = SecurityRequirementsError{}
+
+func (err SecurityRequirementsError) Unwrap() []error {
+	return err.Errors
+}


Test patch:
diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go
index 32cb524..18c28f1 100644
--- a/openapi3/loader_test.go
+++ b/openapi3/loader_test.go
@@ -1,6 +1,7 @@
 package openapi3
 
 import (
+	"bytes"
 	"fmt"
 	"net"
 	"net/http"
@@ -621,3 +622,53 @@ servers:
 		})
 	}
 }
+
+func TestReadFromIoReader(t *testing.T) {
+	buffer := bytes.NewReader([]byte(`openapi: 3.0.0
+info:
+  title: An API
+  version: v1
+components:
+  schemas:
+    NewItem:
+      required: [name]
+      properties:
+        name: {type: string}
+        tag: {type: string}
+    ErrorModel:
+      type: object
+      required: [code, message]
+      properties:
+        code: {type: integer}
+        message: {type: string}
+paths:
+  /items:
+    put:
+      description: ''
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/NewItem'
+      responses:
+        default: &defaultResponse # a YAML ref
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorModel'`))
+
+	loader := NewLoader()
+	doc, err := loader.LoadFromIoReader(buffer)
+	require.NoError(t, err)
+
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+}
+
+func TestReadFromIoReader_Nil(t *testing.T) {
+	loader := NewLoader()
+	_, err := loader.LoadFromIoReader(nil)
+	require.EqualError(t, err, "invalid reader: <nil>")
+}
diff --git a/openapi3filter/validate_request_example_test.go b/openapi3filter/validate_request_example_test.go
new file mode 100644
index 0000000..653db30
--- /dev/null
+++ b/openapi3filter/validate_request_example_test.go
@@ -0,0 +1,117 @@
+package openapi3filter
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func ExampleAuthenticationFunc() {
+	const spec = `
+openapi: 3.0.0
+info:
+  title: 'Validator'
+  version: 0.0.1
+components:
+  securitySchemes:
+    OAuth2:
+      type: oauth2
+      flows:
+        clientCredentials:
+          tokenUrl: /oauth2/token
+          scopes:
+            secrets.read: Ability to read secrets
+            secrets.write: Ability to write secrets
+paths:
+  /secret:
+    post:
+      security:
+        - OAuth2:
+          - secrets.write
+      responses:
+        '200':
+          description: Ok
+        '401':
+          description: Unauthorized
+`
+	var (
+		errUnauthenticated = errors.New("login required")
+		errForbidden       = errors.New("permission denied")
+	)
+
+	userScopes := map[string][]string{
+		"Alice": {"secrets.read"},
+		"Bob":   {"secrets.read", "secrets.write"},
+	}
+
+	authenticationFunc := func(_ context.Context, ai *AuthenticationInput) error {
+		user := ai.RequestValidationInput.Request.Header.Get("X-User")
+		if user == "" {
+			return errUnauthenticated
+		}
+
+		for _, requiredScope := range ai.Scopes {
+			var allowed bool
+			for _, scope := range userScopes[user] {
+				if scope == requiredScope {
+					allowed = true
+					break
+				}
+			}
+			if !allowed {
+				return errForbidden
+			}
+		}
+
+		return nil
+	}
+
+	loader := openapi3.NewLoader()
+	doc, _ := loader.LoadFromData([]byte(spec))
+	router, _ := gorillamux.NewRouter(doc)
+
+	validateRequest := func(req *http.Request) {
+		route, pathParams, _ := router.FindRoute(req)
+
+		validationInput := &RequestValidationInput{
+			Request:    req,
+			PathParams: pathParams,
+			Route:      route,
+			Options: &Options{
+				AuthenticationFunc: authenticationFunc,
+			},
+		}
+		err := ValidateRequest(context.TODO(), validationInput)
+		switch {
+		case errors.Is(err, errUnauthenticated):
+			fmt.Println("username is required")
+		case errors.Is(err, errForbidden):
+			fmt.Println("user is not allowed to perform this action")
+		case err == nil:
+			fmt.Println("ok")
+		default:
+			log.Fatal(err)
+		}
+	}
+
+	req1, _ := http.NewRequest(http.MethodPost, "/secret", nil)
+	req1.Header.Set("X-User", "Alice")
+
+	req2, _ := http.NewRequest(http.MethodPost, "/secret", nil)
+	req2.Header.Set("X-User", "Bob")
+
+	req3, _ := http.NewRequest(http.MethodPost, "/secret", nil)
+
+	validateRequest(req1)
+	validateRequest(req2)
+	validateRequest(req3)
+	// output:
+	// user is not allowed to perform this action
+	// ok
+	// username is required
+}