mirror of
https://github.com/jc21/nginx-proxy-manager.git
synced 2024-08-30 18:22:48 +00:00
Add backend unit tests
This commit is contained in:
parent
72b071dbaa
commit
b123ca4fd0
@ -1,4 +1,4 @@
|
|||||||
version: "2"
|
version: "3"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
default:
|
default:
|
||||||
|
@ -136,7 +136,15 @@ func getCommonArgs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is split out into it's own function so it's testable
|
// This is split out into it's own function so it's testable
|
||||||
func buildCertRequestArgs(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) ([]string, error) {
|
func buildCertRequestArgs(
|
||||||
|
domains []string,
|
||||||
|
method,
|
||||||
|
outputFullchainFile,
|
||||||
|
outputKeyFile string,
|
||||||
|
dnsProvider *dnsprovider.Model,
|
||||||
|
ca *certificateauthority.Model,
|
||||||
|
force bool,
|
||||||
|
) ([]string, error) {
|
||||||
// The argument order matters.
|
// The argument order matters.
|
||||||
// see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode
|
// see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode
|
||||||
// for multiple domains and note that the method of validation is required just after the domain arg, each time.
|
// for multiple domains and note that the method of validation is required just after the domain arg, each time.
|
||||||
|
@ -193,3 +193,33 @@ func TestBuildCertRequestArgs(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAcmeShFilePath(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
path, err := getAcmeShFilePath()
|
||||||
|
assert.Equal(t, "/bin/acme.sh", path)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCommonEnvVars(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
expected := []string{
|
||||||
|
"ACMESH_CONFIG_HOME=/data/.acme.sh/config",
|
||||||
|
"ACMESH_HOME=/data/.acme.sh",
|
||||||
|
"CERT_HOME=/data/.acme.sh/certs",
|
||||||
|
"LE_CONFIG_HOME=/data/.acme.sh/config",
|
||||||
|
"LE_WORKING_DIR=/data/.acme.sh",
|
||||||
|
}
|
||||||
|
vals := getCommonEnvVars()
|
||||||
|
assert.Equal(t, expected, vals)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAcmeShVersion(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
resp := GetAcmeShVersion()
|
||||||
|
assert.Greater(t, len(resp), 1)
|
||||||
|
assert.Equal(t, "v", resp[:1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
13
backend/internal/api/context/context_test.go
Normal file
13
backend/internal/api/context/context_test.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetString(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "context value: Body", BodyCtxKey.String())
|
||||||
|
})
|
||||||
|
}
|
@ -1,208 +0,0 @@
|
|||||||
package filters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware
|
|
||||||
func NewFilterSchema(fieldSchemas []string) string {
|
|
||||||
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// BoolFieldSchema returns the Field Schema for a Boolean accepted value field
|
|
||||||
func BoolFieldSchema(fieldName string) string {
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%s$"
|
|
||||||
},
|
|
||||||
"modifier": %s,
|
|
||||||
"value": {
|
|
||||||
"oneOf": [
|
|
||||||
%s,
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": %s
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, fieldName, boolModifiers, filterBool, filterBool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntFieldSchema returns the Field Schema for a Integer accepted value field
|
|
||||||
func IntFieldSchema(fieldName string) string {
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%s$"
|
|
||||||
},
|
|
||||||
"modifier": %s,
|
|
||||||
"value": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]+$"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]+$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, fieldName, allModifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringFieldSchema returns the Field Schema for a String accepted value field
|
|
||||||
func StringFieldSchema(fieldName string) string {
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%s$"
|
|
||||||
},
|
|
||||||
"modifier": %s,
|
|
||||||
"value": {
|
|
||||||
"oneOf": [
|
|
||||||
%s,
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": %s
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, fieldName, stringModifiers, filterString, filterString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
|
|
||||||
func RegexFieldSchema(fieldName string, regex string) string {
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%s$"
|
|
||||||
},
|
|
||||||
"modifier": %s,
|
|
||||||
"value": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "%s"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "%s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, fieldName, stringModifiers, regex, regex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
|
||||||
func DateFieldSchema(fieldName string) string {
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%s$"
|
|
||||||
},
|
|
||||||
"modifier": %s,
|
|
||||||
"value": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, fieldName, allModifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
|
||||||
// 2020-03-01T10:30:00+10:00
|
|
||||||
func DateTimeFieldSchema(fieldName string) string {
|
|
||||||
return fmt.Sprintf(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"field": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%s$"
|
|
||||||
},
|
|
||||||
"modifier": %s,
|
|
||||||
"value": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, fieldName, allModifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
const allModifiers = `{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
|
|
||||||
}`
|
|
||||||
|
|
||||||
const boolModifiers = `{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^(equals|not)$"
|
|
||||||
}`
|
|
||||||
|
|
||||||
const stringModifiers = `{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
|
|
||||||
}`
|
|
||||||
|
|
||||||
const filterBool = `{
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
|
|
||||||
}`
|
|
||||||
|
|
||||||
const filterString = `{
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
}`
|
|
||||||
|
|
||||||
const baseFilterSchema = `{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
%s
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}`
|
|
@ -26,7 +26,7 @@ func GetCertificates() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
|
certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r), middleware.GetExpandFromContext(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
} else {
|
} else {
|
||||||
@ -41,7 +41,7 @@ func GetCertificate() func(http.ResponseWriter, *http.Request) {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if item := getCertificateFromRequest(w, r); item != nil {
|
if item := getCertificateFromRequest(w, r); item != nil {
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
item.Expand(getExpandFromContext(r))
|
item.Expand(middleware.GetExpandFromContext(r))
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"npm/internal/api/context"
|
|
||||||
"npm/internal/api/middleware"
|
|
||||||
"npm/internal/model"
|
"npm/internal/model"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@ -15,7 +13,7 @@ import (
|
|||||||
const defaultLimit = 10
|
const defaultLimit = 10
|
||||||
|
|
||||||
func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
|
func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
|
||||||
var pageInfo model.PageInfo
|
pageInfo := model.PageInfo{}
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
pageInfo.Offset, pageInfo.Limit, err = getPagination(r)
|
pageInfo.Offset, pageInfo.Limit, err = getPagination(r)
|
||||||
@ -23,7 +21,7 @@ func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
|
|||||||
return pageInfo, err
|
return pageInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pageInfo.Sort = middleware.GetSortFromContext(r)
|
// pageInfo.Sort = middleware.GetSortFromContext(r)
|
||||||
|
|
||||||
return pageInfo, nil
|
return pageInfo, nil
|
||||||
}
|
}
|
||||||
@ -93,12 +91,3 @@ func getPagination(r *http.Request) (int, int, error) {
|
|||||||
|
|
||||||
return offset, limit, nil
|
return offset, limit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getExpandFromContext returns the Expansion setting
|
|
||||||
func getExpandFromContext(r *http.Request) []string {
|
|
||||||
expand, ok := r.Context().Value(context.ExpansionCtxKey).([]string)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return expand
|
|
||||||
}
|
|
||||||
|
118
backend/internal/api/handler/helpers_test.go
Normal file
118
backend/internal/api/handler/helpers_test.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"npm/internal/model"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetPageInfoFromRequest(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/hosts", nil)
|
||||||
|
p, err := getPageInfoFromRequest(r)
|
||||||
|
|
||||||
|
var nilStringSlice []string
|
||||||
|
var nilSortSlice []model.Sort
|
||||||
|
defaultSort := model.Sort{Field: "name", Direction: "asc"}
|
||||||
|
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, 0, p.Offset)
|
||||||
|
assert.Equal(t, 10, p.Limit)
|
||||||
|
assert.Equal(t, nilStringSlice, p.Expand)
|
||||||
|
assert.Equal(t, nilSortSlice, p.Sort)
|
||||||
|
assert.Equal(t, []model.Sort{defaultSort}, p.GetSort(defaultSort))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetQueryVarInt(t *testing.T) {
|
||||||
|
type want struct {
|
||||||
|
val int
|
||||||
|
err string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
queryVar string
|
||||||
|
required bool
|
||||||
|
defaultValue int
|
||||||
|
want want
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple default",
|
||||||
|
url: "/hosts",
|
||||||
|
queryVar: "something",
|
||||||
|
required: false,
|
||||||
|
defaultValue: 100,
|
||||||
|
want: want{
|
||||||
|
val: 100,
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "required flag",
|
||||||
|
url: "/hosts",
|
||||||
|
queryVar: "something",
|
||||||
|
required: true,
|
||||||
|
want: want{
|
||||||
|
val: 0,
|
||||||
|
err: "something was not supplied in the request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple get",
|
||||||
|
url: "/hosts?something=50",
|
||||||
|
queryVar: "something",
|
||||||
|
required: true,
|
||||||
|
want: want{
|
||||||
|
val: 50,
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid number",
|
||||||
|
url: "/hosts?something=aaa",
|
||||||
|
queryVar: "something",
|
||||||
|
required: true,
|
||||||
|
want: want{
|
||||||
|
val: 0,
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preceding zeros",
|
||||||
|
url: "/hosts?something=0000050",
|
||||||
|
queryVar: "something",
|
||||||
|
required: true,
|
||||||
|
want: want{
|
||||||
|
val: 50,
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "decimals",
|
||||||
|
url: "/hosts?something=50.50",
|
||||||
|
queryVar: "something",
|
||||||
|
required: true,
|
||||||
|
want: want{
|
||||||
|
val: 0,
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, tt.url, nil)
|
||||||
|
val, err := getQueryVarInt(r, tt.queryVar, tt.required, tt.defaultValue)
|
||||||
|
assert.Equal(t, tt.want.val, val)
|
||||||
|
if tt.want.err != "" {
|
||||||
|
assert.NotEqual(t, nil, err)
|
||||||
|
assert.Equal(t, tt.want.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ func GetHosts() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
|
hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r), middleware.GetExpandFromContext(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
} else {
|
} else {
|
||||||
@ -52,7 +52,7 @@ func GetHost() func(http.ResponseWriter, *http.Request) {
|
|||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
item.Expand(getExpandFromContext(r))
|
item.Expand(middleware.GetExpandFromContext(r))
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
default:
|
default:
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
@ -132,7 +132,7 @@ func UpdateHost() func(http.ResponseWriter, *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
hostObject.Expand(getExpandFromContext(r))
|
hostObject.Expand(middleware.GetExpandFromContext(r))
|
||||||
|
|
||||||
configureHost(hostObject)
|
configureHost(hostObject)
|
||||||
|
|
||||||
|
@ -25,14 +25,21 @@ func NotFound() func(http.ResponseWriter, *http.Request) {
|
|||||||
assetsSub, _ = fs.Sub(embed.Assets, "assets")
|
assetsSub, _ = fs.Sub(embed.Assets, "assets")
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defaultFile := "index.html"
|
||||||
path := strings.TrimLeft(r.URL.Path, "/")
|
path := strings.TrimLeft(r.URL.Path, "/")
|
||||||
|
|
||||||
|
isAPI := false
|
||||||
|
if len(path) >= 3 && path[0:3] == "api" {
|
||||||
|
isAPI = true
|
||||||
|
}
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "index.html"
|
path = defaultFile
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tryRead(assetsSub, path, w)
|
err := tryRead(assetsSub, path, w)
|
||||||
if err == errIsDir {
|
if err == errIsDir {
|
||||||
err = tryRead(assetsSub, "index.html", w)
|
err = tryRead(assetsSub, defaultFile, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
}
|
}
|
||||||
@ -40,6 +47,16 @@ func NotFound() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the path has an extension and not in the "/api" path
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
if !isAPI && ext == "" {
|
||||||
|
// Not an api endpoint and Not a specific file, return the default index file
|
||||||
|
err := tryRead(assetsSub, defaultFile, w)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func GetUpstreams() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
items, err := upstream.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
|
items, err := upstream.List(pageInfo, middleware.GetFiltersFromContext(r), middleware.GetExpandFromContext(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
} else {
|
} else {
|
||||||
@ -53,7 +53,7 @@ func GetUpstream() func(http.ResponseWriter, *http.Request) {
|
|||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
item.Expand(getExpandFromContext(r))
|
item.Expand(middleware.GetExpandFromContext(r))
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
default:
|
default:
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
@ -127,7 +127,7 @@ func UpdateUpstream() func(http.ResponseWriter, *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
// item.Expand(getExpandFromContext(r))
|
// item.Expand(middleware.GetExpandFromContext(r))
|
||||||
|
|
||||||
configureUpstream(item)
|
configureUpstream(item)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ func GetUsers() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
|
users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r), middleware.GetExpandFromContext(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
} else {
|
} else {
|
||||||
@ -52,7 +52,7 @@ func GetUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
item.Expand(getExpandFromContext(r))
|
item.Expand(middleware.GetExpandFromContext(r))
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
default:
|
default:
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
@ -108,7 +108,7 @@ func UpdateUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
userObject.Expand(getExpandFromContext(r))
|
userObject.Expand(middleware.GetExpandFromContext(r))
|
||||||
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
|
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
|
||||||
default:
|
default:
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/qri-io/jsonschema"
|
|
||||||
"github.com/rotisserie/eris"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrInvalidJSON is an error for invalid json
|
|
||||||
ErrInvalidJSON = eris.New("JSON is invalid")
|
|
||||||
// ErrInvalidPayload is an error for invalid incoming data
|
|
||||||
ErrInvalidPayload = eris.New("Payload is invalid")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateRequestSchema takes a Schema and the Content to validate against it
|
|
||||||
func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) {
|
|
||||||
var jsonErrors []jsonschema.KeyError
|
|
||||||
var schemaBytes = []byte(schema)
|
|
||||||
|
|
||||||
// Make sure the body is valid JSON
|
|
||||||
if !isJSON(requestBody) {
|
|
||||||
return jsonErrors, ErrInvalidJSON
|
|
||||||
}
|
|
||||||
|
|
||||||
rs := &jsonschema.Schema{}
|
|
||||||
if err := json.Unmarshal(schemaBytes, rs); err != nil {
|
|
||||||
return jsonErrors, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var validationErr error
|
|
||||||
ctx := context.TODO()
|
|
||||||
if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 {
|
|
||||||
return jsonErrors, validationErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isJSON(bytes []byte) bool {
|
|
||||||
var js map[string]interface{}
|
|
||||||
return json.Unmarshal(bytes, &js) == nil
|
|
||||||
}
|
|
@ -11,6 +11,12 @@ import (
|
|||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
"github.com/qri-io/jsonschema"
|
"github.com/qri-io/jsonschema"
|
||||||
|
"github.com/rotisserie/eris"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidPayload is an error for invalid incoming data
|
||||||
|
ErrInvalidPayload = eris.New("Payload is invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response interface for standard API results
|
// Response interface for standard API results
|
||||||
|
180
backend/internal/api/http/responses_test.go
Normal file
180
backend/internal/api/http/responses_test.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"npm/internal/entity/user"
|
||||||
|
"npm/internal/model"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/qri-io/jsonschema"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResultResponseJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status int
|
||||||
|
given interface{}
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple response",
|
||||||
|
status: http.StatusOK,
|
||||||
|
given: true,
|
||||||
|
want: "{\"result\":true}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "detailed response",
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
given: user.Model{
|
||||||
|
ModelBase: model.ModelBase{ID: 10},
|
||||||
|
Email: "me@example.com",
|
||||||
|
Name: "John Doe",
|
||||||
|
Nickname: "Jonny",
|
||||||
|
},
|
||||||
|
want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"nickname\":\"Jonny\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error response",
|
||||||
|
status: http.StatusNotFound,
|
||||||
|
given: ErrorResponse{
|
||||||
|
Code: 404,
|
||||||
|
Message: "Not found",
|
||||||
|
Invalid: []string{"your", "page", "was", "not", "found"},
|
||||||
|
},
|
||||||
|
want: "{\"result\":null,\"error\":{\"code\":404,\"message\":\"Not found\",\"invalid\":[\"your\",\"page\",\"was\",\"not\",\"found\"]}}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/anything", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ResultResponseJSON(w, r, tt.status, tt.given)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil got %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(body))
|
||||||
|
assert.Equal(t, tt.status, res.StatusCode)
|
||||||
|
assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultSchemaErrorJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
given []jsonschema.KeyError
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "case a",
|
||||||
|
given: []jsonschema.KeyError{
|
||||||
|
{
|
||||||
|
PropertyPath: "/something",
|
||||||
|
InvalidValue: "name",
|
||||||
|
Message: "Name cannot be empty",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"result\":null,\"error\":{\"code\":400,\"message\":{},\"invalid\":[{\"propertyPath\":\"/something\",\"invalidValue\":\"name\",\"message\":\"Name cannot be empty\"}]}}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/anything", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ResultSchemaErrorJSON(w, r, tt.given)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil got %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(body))
|
||||||
|
assert.Equal(t, 400, res.StatusCode)
|
||||||
|
assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultErrorJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status int
|
||||||
|
message string
|
||||||
|
extended interface{}
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "case a",
|
||||||
|
status: http.StatusBadGateway,
|
||||||
|
message: "Oh not something is not acceptable",
|
||||||
|
extended: nil,
|
||||||
|
want: "{\"result\":null,\"error\":{\"code\":502,\"message\":\"Oh not something is not acceptable\"}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case b",
|
||||||
|
status: http.StatusNotAcceptable,
|
||||||
|
message: "Oh not something is not acceptable again",
|
||||||
|
extended: []string{"name is not allowed", "dob is wrong or something"},
|
||||||
|
want: "{\"result\":null,\"error\":{\"code\":406,\"message\":\"Oh not something is not acceptable again\",\"invalid\":[\"name is not allowed\",\"dob is wrong or something\"]}}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/anything", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ResultErrorJSON(w, r, tt.status, tt.message, tt.extended)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil got %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(body))
|
||||||
|
assert.Equal(t, tt.status, res.StatusCode)
|
||||||
|
assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotFound(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/anything", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
NotFound(w, r)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil got %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, "{\"result\":null,\"error\":{\"code\":404,\"message\":\"Not found\"}}", string(body))
|
||||||
|
assert.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||||
|
assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultResponseText(t *testing.T) {
|
||||||
|
t.Run("basic test", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/anything", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ResultResponseText(w, r, http.StatusOK, "omg this works")
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil got %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, "omg this works", string(body))
|
||||||
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||||
|
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
}
|
@ -22,3 +22,12 @@ func Expansion(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExpandFromContext returns the Expansion setting
|
||||||
|
func GetExpandFromContext(r *http.Request) []string {
|
||||||
|
expand, ok := r.Context().Value(c.ExpansionCtxKey).([]string)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return expand
|
||||||
|
}
|
||||||
|
@ -35,7 +35,7 @@ func TestGetHealthz(t *testing.T) {
|
|||||||
|
|
||||||
func TestNonExistent(t *testing.T) {
|
func TestNonExistent(t *testing.T) {
|
||||||
respRec := httptest.NewRecorder()
|
respRec := httptest.NewRecorder()
|
||||||
req, _ := http.NewRequest("GET", "/non-existent-endpoint", nil)
|
req, _ := http.NewRequest("GET", "/non-existent-endpoint.jpg", nil)
|
||||||
|
|
||||||
r.ServeHTTP(respRec, req)
|
r.ServeHTTP(respRec, req)
|
||||||
assert.Equal(t, http.StatusNotFound, respRec.Code)
|
assert.Equal(t, http.StatusNotFound, respRec.Code)
|
||||||
|
Loading…
Reference in New Issue
Block a user