mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-07 16:54:16 +01:00
Add routing Rest API support (#428)
Routing API will allow us to list, create, update, and delete routes.
This commit is contained in:
parent
4b34a6d6df
commit
000ea72aec
@ -14,6 +14,8 @@ tags:
|
||||
description: Interact with and view information about groups.
|
||||
- name: Rules
|
||||
description: Interact with and view information about rules.
|
||||
- name: Routes
|
||||
description: Interact with and view information about routes.
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
@ -191,17 +193,13 @@ components:
|
||||
$ref: '#/components/schemas/PeerMinimum'
|
||||
required:
|
||||
- peers
|
||||
GroupPatchOperation:
|
||||
PatchMinimum:
|
||||
type: object
|
||||
properties:
|
||||
op:
|
||||
description: Patch operation type
|
||||
type: string
|
||||
enum: [ "replace","add","remove" ]
|
||||
path:
|
||||
description: Group field to update in form /<field>
|
||||
type: string
|
||||
enum: [ "name","peers" ]
|
||||
value:
|
||||
description: Values to be applied
|
||||
type: array
|
||||
@ -209,8 +207,19 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- op
|
||||
- path
|
||||
- value
|
||||
GroupPatchOperation:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PatchMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
path:
|
||||
description: Group field to update in form /<field>
|
||||
type: string
|
||||
enum: [ "name","peers" ]
|
||||
required:
|
||||
- path
|
||||
|
||||
RuleMinimum:
|
||||
type: object
|
||||
properties:
|
||||
@ -257,25 +266,73 @@ components:
|
||||
- sources
|
||||
- destinations
|
||||
RulePatchOperation:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PatchMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
path:
|
||||
description: Rule field to update in form /<field>
|
||||
type: string
|
||||
enum: [ "name","description","disabled","flow","sources","destinations" ]
|
||||
required:
|
||||
- path
|
||||
RouteRequest:
|
||||
type: object
|
||||
properties:
|
||||
op:
|
||||
description: Patch operation type
|
||||
description:
|
||||
description: Route description
|
||||
type: string
|
||||
enum: [ "replace","add","remove" ]
|
||||
path:
|
||||
description: Rule field to update in form /<field>
|
||||
enabled:
|
||||
description: Route status
|
||||
type: boolean
|
||||
peer:
|
||||
description: Peer Identifier associated with route
|
||||
type: string
|
||||
enum: [ "name","description","disabled","flow","sources","destinations" ]
|
||||
value:
|
||||
description: Values to be applied
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
prefix:
|
||||
description: Prefix or network range in CIDR format
|
||||
type: string
|
||||
metric:
|
||||
description: Route metric number. Lowest number has higher priority
|
||||
type: integer
|
||||
maximum: 9999
|
||||
minimum: 1
|
||||
masquerade:
|
||||
description: Indicate if peer should masquerade traffic to this route's prefix
|
||||
type: boolean
|
||||
required:
|
||||
- op
|
||||
- path
|
||||
- value
|
||||
- id
|
||||
- description
|
||||
- enabled
|
||||
- peer
|
||||
- prefix
|
||||
- metric
|
||||
- masquerade
|
||||
Route:
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
description: Route Id
|
||||
type: string
|
||||
prefix_type:
|
||||
description: Prefix type indicating if it is IPv4 or IPv6
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- prefix_type
|
||||
- $ref: '#/components/schemas/RouteRequest'
|
||||
RoutePatchOperation:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PatchMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
path:
|
||||
description: Route field to update in form /<field>
|
||||
type: string
|
||||
enum: [ "prefix","description","enabled","peer","metric","masquerade" ]
|
||||
required:
|
||||
- path
|
||||
|
||||
responses:
|
||||
not_found:
|
||||
description: Resource not found
|
||||
@ -945,5 +1002,176 @@ paths:
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
|
||||
/api/routes:
|
||||
get:
|
||||
summary: Returns a list of all routes
|
||||
tags: [ Routes ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON Array of Routes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Route'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Creates a Route
|
||||
tags: [ Routes ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
requestBody:
|
||||
description: New Routes request
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/RouteRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Route Object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Route'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
|
||||
/api/routes/{id}:
|
||||
get:
|
||||
summary: Get information about a Routes
|
||||
tags: [ Routes ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Route ID
|
||||
responses:
|
||||
'200':
|
||||
description: A Route object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Route'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
summary: Update/Replace a Route
|
||||
tags: [ Routes ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Route ID
|
||||
requestBody:
|
||||
description: Update Route request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RouteRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Route object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Route'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
patch:
|
||||
summary: Update information about a Route
|
||||
tags: [ Routes ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Route ID
|
||||
requestBody:
|
||||
description: Update Route request using a list of json patch objects
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RoutePatchOperation'
|
||||
responses:
|
||||
'200':
|
||||
description: A Route object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Route'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete a Route
|
||||
tags: [ Routes ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Route ID
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: { }
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
@ -24,6 +24,30 @@ const (
|
||||
GroupPatchOperationPathPeers GroupPatchOperationPath = "peers"
|
||||
)
|
||||
|
||||
// Defines values for PatchMinimumOp.
|
||||
const (
|
||||
PatchMinimumOpAdd PatchMinimumOp = "add"
|
||||
PatchMinimumOpRemove PatchMinimumOp = "remove"
|
||||
PatchMinimumOpReplace PatchMinimumOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for RoutePatchOperationOp.
|
||||
const (
|
||||
RoutePatchOperationOpAdd RoutePatchOperationOp = "add"
|
||||
RoutePatchOperationOpRemove RoutePatchOperationOp = "remove"
|
||||
RoutePatchOperationOpReplace RoutePatchOperationOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for RoutePatchOperationPath.
|
||||
const (
|
||||
RoutePatchOperationPathDescription RoutePatchOperationPath = "description"
|
||||
RoutePatchOperationPathEnabled RoutePatchOperationPath = "enabled"
|
||||
RoutePatchOperationPathMasquerade RoutePatchOperationPath = "masquerade"
|
||||
RoutePatchOperationPathMetric RoutePatchOperationPath = "metric"
|
||||
RoutePatchOperationPathPeer RoutePatchOperationPath = "peer"
|
||||
RoutePatchOperationPathPrefix RoutePatchOperationPath = "prefix"
|
||||
)
|
||||
|
||||
// Defines values for RulePatchOperationOp.
|
||||
const (
|
||||
RulePatchOperationOpAdd RulePatchOperationOp = "add"
|
||||
@ -86,6 +110,18 @@ type GroupPatchOperationOp string
|
||||
// Group field to update in form /<field>
|
||||
type GroupPatchOperationPath string
|
||||
|
||||
// PatchMinimum defines model for PatchMinimum.
|
||||
type PatchMinimum struct {
|
||||
// Patch operation type
|
||||
Op PatchMinimumOp `json:"op"`
|
||||
|
||||
// Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// Patch operation type
|
||||
type PatchMinimumOp string
|
||||
|
||||
// Peer defines model for Peer.
|
||||
type Peer struct {
|
||||
// Provides information of who activated the Peer. User or Setup Key
|
||||
@ -131,6 +167,72 @@ type PeerMinimum struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Route defines model for Route.
|
||||
type Route struct {
|
||||
// Route description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Route status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Route Id
|
||||
Id string `json:"id"`
|
||||
|
||||
// Indicate if peer should masquerade traffic to this route's prefix
|
||||
Masquerade bool `json:"masquerade"`
|
||||
|
||||
// Route metric number. Lowest number has higher priority
|
||||
Metric int `json:"metric"`
|
||||
|
||||
// Peer Identifier associated with route
|
||||
Peer string `json:"peer"`
|
||||
|
||||
// Prefix or network range in CIDR format
|
||||
Prefix string `json:"prefix"`
|
||||
|
||||
// Prefix type indicating if it is IPv4 or IPv6
|
||||
PrefixType string `json:"prefix_type"`
|
||||
}
|
||||
|
||||
// RoutePatchOperation defines model for RoutePatchOperation.
|
||||
type RoutePatchOperation struct {
|
||||
// Patch operation type
|
||||
Op RoutePatchOperationOp `json:"op"`
|
||||
|
||||
// Route field to update in form /<field>
|
||||
Path RoutePatchOperationPath `json:"path"`
|
||||
|
||||
// Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// Patch operation type
|
||||
type RoutePatchOperationOp string
|
||||
|
||||
// Route field to update in form /<field>
|
||||
type RoutePatchOperationPath string
|
||||
|
||||
// RouteRequest defines model for RouteRequest.
|
||||
type RouteRequest struct {
|
||||
// Route description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Route status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Indicate if peer should masquerade traffic to this route's prefix
|
||||
Masquerade bool `json:"masquerade"`
|
||||
|
||||
// Route metric number. Lowest number has higher priority
|
||||
Metric int `json:"metric"`
|
||||
|
||||
// Peer Identifier associated with route
|
||||
Peer string `json:"peer"`
|
||||
|
||||
// Prefix or network range in CIDR format
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
// Rule defines model for Rule.
|
||||
type Rule struct {
|
||||
// Rule friendly description
|
||||
@ -272,6 +374,15 @@ type PutApiPeersIdJSONBody struct {
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
}
|
||||
|
||||
// PostApiRoutesJSONBody defines parameters for PostApiRoutes.
|
||||
type PostApiRoutesJSONBody = RouteRequest
|
||||
|
||||
// PatchApiRoutesIdJSONBody defines parameters for PatchApiRoutesId.
|
||||
type PatchApiRoutesIdJSONBody = []RoutePatchOperation
|
||||
|
||||
// PutApiRoutesIdJSONBody defines parameters for PutApiRoutesId.
|
||||
type PutApiRoutesIdJSONBody = RouteRequest
|
||||
|
||||
// PostApiRulesJSONBody defines parameters for PostApiRules.
|
||||
type PostApiRulesJSONBody struct {
|
||||
// Rule friendly description
|
||||
@ -327,6 +438,15 @@ type PutApiGroupsIdJSONRequestBody PutApiGroupsIdJSONBody
|
||||
// PutApiPeersIdJSONRequestBody defines body for PutApiPeersId for application/json ContentType.
|
||||
type PutApiPeersIdJSONRequestBody PutApiPeersIdJSONBody
|
||||
|
||||
// PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType.
|
||||
type PostApiRoutesJSONRequestBody = PostApiRoutesJSONBody
|
||||
|
||||
// PatchApiRoutesIdJSONRequestBody defines body for PatchApiRoutesId for application/json ContentType.
|
||||
type PatchApiRoutesIdJSONRequestBody = PatchApiRoutesIdJSONBody
|
||||
|
||||
// PutApiRoutesIdJSONRequestBody defines body for PutApiRoutesId for application/json ContentType.
|
||||
type PutApiRoutesIdJSONRequestBody = PutApiRoutesIdJSONBody
|
||||
|
||||
// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType.
|
||||
type PostApiRulesJSONRequestBody PostApiRulesJSONBody
|
||||
|
||||
|
@ -33,6 +33,7 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
|
||||
peersHandler := NewPeers(accountManager, authAudience)
|
||||
keysHandler := NewSetupKeysHandler(accountManager, authAudience)
|
||||
userHandler := NewUserHandler(accountManager, authAudience)
|
||||
routesHandler := NewRoutes(accountManager, authAudience)
|
||||
|
||||
apiHandler.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
|
||||
@ -59,6 +60,13 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
|
||||
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.GetGroupHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS")
|
||||
|
||||
apiHandler.HandleFunc("/api/routes", routesHandler.GetAllRoutesHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes", routesHandler.CreateRouteHandler).Methods("POST", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.UpdateRouteHandler).Methods("PUT", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.PatchRouteHandler).Methods("PATCH", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.GetRouteHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.DeleteRouteHandler).Methods("DELETE", "OPTIONS")
|
||||
|
||||
return apiHandler, nil
|
||||
|
||||
}
|
||||
|
380
management/server/http/routes.go
Normal file
380
management/server/http/routes.go
Normal file
@ -0,0 +1,380 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Routes is the routes handler of the account
|
||||
type Routes struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
// NewRoutes returns a new instance of Routes handler
|
||||
func NewRoutes(accountManager server.AccountManager, authAudience string) *Routes {
|
||||
return &Routes{
|
||||
accountManager: accountManager,
|
||||
authAudience: authAudience,
|
||||
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllRoutesHandler returns the list of routes for the account
|
||||
func (h *Routes) GetAllRoutesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routes, err := h.accountManager.ListRoutes(account.Id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
apiRoutes := make([]*api.Route, 0)
|
||||
for _, r := range routes {
|
||||
apiRoutes = append(apiRoutes, toRouteResponse(account, r))
|
||||
}
|
||||
|
||||
writeJSONObject(w, apiRoutes)
|
||||
}
|
||||
|
||||
// CreateRouteHandler handles route creation request
|
||||
func (h *Routes) CreateRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiRoutesJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
peerKey := req.Peer
|
||||
if req.Peer != "" {
|
||||
peer, err := h.accountManager.GetPeerByIP(account.Id, req.Peer)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
peerKey = peer.Key
|
||||
}
|
||||
|
||||
_, newPrefix, err := route.ParsePrefix(req.Prefix)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("couldn't parse update prefix %s", req.Prefix), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), peerKey, req.Description, req.Masquerade, req.Metric, req.Enabled)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRouteResponse(account, newRoute)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// UpdateRouteHandler handles update to a route identified by a given ID
|
||||
func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
routeID := vars["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route Id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.accountManager.GetRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("couldn't find route for ID %s", routeID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PutApiRoutesIdJSONBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
prefixType, newPrefix, err := route.ParsePrefix(req.Prefix)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("couldn't parse update prefix %s for route ID %s", req.Prefix, routeID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
peerKey := req.Peer
|
||||
if req.Peer != "" {
|
||||
peer, err := h.accountManager.GetPeerByIP(account.Id, req.Peer)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
peerKey = peer.Key
|
||||
}
|
||||
|
||||
newRoute := &route.Route{
|
||||
ID: routeID,
|
||||
Prefix: newPrefix,
|
||||
PrefixType: prefixType,
|
||||
Masquerade: req.Masquerade,
|
||||
Peer: peerKey,
|
||||
Metric: req.Metric,
|
||||
Description: req.Description,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
|
||||
err = h.accountManager.SaveRoute(account.Id, newRoute)
|
||||
if err != nil {
|
||||
log.Errorf("failed updating route \"%s\" under account %s %v", routeID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRouteResponse(account, newRoute)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// PatchRouteHandler handles patch updates to a route identified by a given ID
|
||||
func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
routeID := vars["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.accountManager.GetRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, fmt.Sprintf("couldn't find route ID %s", routeID), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PatchApiRoutesIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req) == 0 {
|
||||
http.Error(w, "no patch instruction received", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var operations []server.RouteUpdateOperation
|
||||
|
||||
for _, patch := range req {
|
||||
switch patch.Path {
|
||||
case api.RoutePatchOperationPathPrefix:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Prefix field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRoutePrefix,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathDescription:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Description field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteDescription,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathPeer:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Peer field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(patch.Value) > 1 {
|
||||
http.Error(w, fmt.Sprintf("Value field only accepts 1 value, got %d", len(patch.Value)),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
peerValue := patch.Value
|
||||
if patch.Value[0] != "" {
|
||||
peer, err := h.accountManager.GetPeerByIP(account.Id, patch.Value[0])
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
peerValue = []string{peer.Key}
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRoutePeer,
|
||||
Values: peerValue,
|
||||
})
|
||||
case api.RoutePatchOperationPathMetric:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Metric field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteMetric,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathMasquerade:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Masquerade field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteMasquerade,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RoutePatchOperationPathEnabled:
|
||||
if patch.Op != api.RoutePatchOperationOpReplace {
|
||||
http.Error(w, fmt.Sprintf("Enabled field only accepts replace operation, got %s", patch.Op),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RouteUpdateOperation{
|
||||
Type: server.UpdateRouteEnabled,
|
||||
Values: patch.Value,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "invalid patch path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
route, err := h.accountManager.UpdateRoute(account.Id, routeID, operations)
|
||||
|
||||
if err != nil {
|
||||
errStatus, ok := status.FromError(err)
|
||||
if ok && errStatus.Code() == codes.Internal {
|
||||
http.Error(w, errStatus.String(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.NotFound {
|
||||
http.Error(w, errStatus.String(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && errStatus.Code() == codes.InvalidArgument {
|
||||
http.Error(w, errStatus.String(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("failed updating route %s under account %s %v", routeID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRouteResponse(account, route)
|
||||
|
||||
writeJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// DeleteRouteHandler handles route deletion request
|
||||
func (h *Routes) DeleteRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routeID := mux.Vars(r)["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.accountManager.DeleteRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
log.Errorf("failed delete route %s under account %s %v", routeID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
// GetRouteHandler handles a route Get request identified by ID
|
||||
func (h *Routes) GetRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
routeID := mux.Vars(r)["id"]
|
||||
if len(routeID) == 0 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
foundRoute, err := h.accountManager.GetRoute(account.Id, routeID)
|
||||
if err != nil {
|
||||
http.Error(w, "route not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toRouteResponse(account, foundRoute))
|
||||
}
|
||||
|
||||
func toRouteResponse(account *server.Account, serverRoute *route.Route) *api.Route {
|
||||
var peerIP string
|
||||
if serverRoute.Peer != "" {
|
||||
peer, found := account.Peers[serverRoute.Peer]
|
||||
if !found {
|
||||
panic("peer ID not found")
|
||||
}
|
||||
peerIP = peer.IP.String()
|
||||
}
|
||||
|
||||
return &api.Route{
|
||||
Id: serverRoute.ID,
|
||||
Description: serverRoute.Description,
|
||||
Enabled: serverRoute.Enabled,
|
||||
Peer: peerIP,
|
||||
Prefix: serverRoute.Prefix.String(),
|
||||
PrefixType: serverRoute.PrefixType.String(),
|
||||
Masquerade: serverRoute.Masquerade,
|
||||
Metric: serverRoute.Metric,
|
||||
}
|
||||
}
|
338
management/server/http/routes_test.go
Normal file
338
management/server/http/routes_test.go
Normal file
@ -0,0 +1,338 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
)
|
||||
|
||||
const (
|
||||
existingRouteID = "existingRouteID"
|
||||
notFoundRouteID = "notFoundRouteID"
|
||||
existingPeerID = "100.64.0.100"
|
||||
notFoundPeerID = "100.64.0.200"
|
||||
existingPeerKey = "existingPeerKey"
|
||||
testAccountID = "test_id"
|
||||
)
|
||||
|
||||
var baseExistingRoute = &route.Route{
|
||||
ID: existingRouteID,
|
||||
Description: "base route",
|
||||
Prefix: netip.MustParsePrefix("192.168.0.0/24"),
|
||||
PrefixType: route.IPv4Prefix,
|
||||
Metric: 9999,
|
||||
Masquerade: false,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
var testingAccount = &server.Account{
|
||||
Id: testAccountID,
|
||||
Domain: "hotmail.com",
|
||||
Peers: map[string]*server.Peer{
|
||||
existingPeerKey: {
|
||||
Key: existingPeerID,
|
||||
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func initRoutesTestData() *Routes {
|
||||
return &Routes{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetRouteFunc: func(_, routeID string) (*route.Route, error) {
|
||||
if routeID == existingRouteID {
|
||||
return baseExistingRoute, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "route with ID %s not found", routeID)
|
||||
},
|
||||
CreateRouteFunc: func(accountID string, prefix, peer, description string, masquerade bool, metric int, enabled bool) (*route.Route, error) {
|
||||
prefixType, p, _ := route.ParsePrefix(prefix)
|
||||
return &route.Route{
|
||||
ID: existingRouteID,
|
||||
Peer: peer,
|
||||
Prefix: p,
|
||||
PrefixType: prefixType,
|
||||
Description: description,
|
||||
Masquerade: masquerade,
|
||||
Enabled: enabled,
|
||||
}, nil
|
||||
},
|
||||
SaveRouteFunc: func(_ string, _ *route.Route) error {
|
||||
return nil
|
||||
},
|
||||
DeleteRouteFunc: func(_ string, _ string) error {
|
||||
return nil
|
||||
},
|
||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
||||
if peerIP != existingPeerID {
|
||||
return nil, status.Errorf(codes.NotFound, "Peer with ID %s not found", peerIP)
|
||||
}
|
||||
return &server.Peer{
|
||||
Key: existingPeerKey,
|
||||
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
|
||||
}, nil
|
||||
},
|
||||
UpdateRouteFunc: func(_ string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) {
|
||||
routeToUpdate := baseExistingRoute
|
||||
if routeID != routeToUpdate.ID {
|
||||
return nil, status.Errorf(codes.NotFound, "route %s no longer exists", routeID)
|
||||
}
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case server.UpdateRoutePrefix:
|
||||
routeToUpdate.PrefixType, routeToUpdate.Prefix, _ = route.ParsePrefix(operation.Values[0])
|
||||
case server.UpdateRouteDescription:
|
||||
routeToUpdate.Description = operation.Values[0]
|
||||
case server.UpdateRoutePeer:
|
||||
routeToUpdate.Peer = operation.Values[0]
|
||||
case server.UpdateRouteMetric:
|
||||
routeToUpdate.Metric, _ = strconv.Atoi(operation.Values[0])
|
||||
case server.UpdateRouteMasquerade:
|
||||
routeToUpdate.Masquerade, _ = strconv.ParseBool(operation.Values[0])
|
||||
case server.UpdateRouteEnabled:
|
||||
routeToUpdate.Enabled, _ = strconv.ParseBool(operation.Values[0])
|
||||
default:
|
||||
return nil, fmt.Errorf("no operation")
|
||||
}
|
||||
}
|
||||
return routeToUpdate, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return testingAccount, nil
|
||||
},
|
||||
},
|
||||
authAudience: "",
|
||||
jwtExtractor: jwtclaims.ClaimsExtractor{
|
||||
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
|
||||
return jwtclaims.AuthorizationClaims{
|
||||
UserId: "test_user",
|
||||
Domain: "hotmail.com",
|
||||
AccountId: testAccountID,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutesHandlers(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
expectedRoute *api.Route
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "Get Existing Route",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: toRouteResponse(testingAccount, baseExistingRoute),
|
||||
},
|
||||
{
|
||||
name: "Get Not Existing Route",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/rules/" + notFoundRouteID,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "Delete Existing Route",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Delete Not Existing Route",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/api/rules/" + notFoundRouteID,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "POST OK",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/16\",\"Peer\":\"%s\"}", existingPeerID))),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "Post",
|
||||
Prefix: "192.168.0.0/16",
|
||||
Peer: existingPeerID,
|
||||
PrefixType: route.IPv4PrefixString,
|
||||
Masquerade: false,
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST Not Found Peer",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/16\",\"Peer\":\"%s\"}", notFoundPeerID)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "POST Invalid Prefix",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/routes",
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/34\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/16\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "Post",
|
||||
Prefix: "192.168.0.0/16",
|
||||
Peer: existingPeerID,
|
||||
PrefixType: route.IPv4PrefixString,
|
||||
Masquerade: false,
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PUT Not Found Route",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + notFoundRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/16\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT Not Found Peer",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/16\",\"Peer\":\"%s\"}", notFoundPeerID)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PUT Invalid Prefix",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Prefix\":\"192.168.0.0/34\",\"Peer\":\"%s\"}", existingPeerID)),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PATCH Description OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString("[{\"op\":\"replace\",\"path\":\"description\",\"value\":[\"NewDesc\"]}]"),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "NewDesc",
|
||||
Prefix: baseExistingRoute.Prefix.String(),
|
||||
PrefixType: route.IPv4PrefixString,
|
||||
Masquerade: baseExistingRoute.Masquerade,
|
||||
Enabled: baseExistingRoute.Enabled,
|
||||
Metric: baseExistingRoute.Metric,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PATCH Peer OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("[{\"op\":\"replace\",\"path\":\"peer\",\"value\":[\"%s\"]}]", existingPeerID)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRoute: &api.Route{
|
||||
Id: existingRouteID,
|
||||
Description: "NewDesc",
|
||||
Prefix: baseExistingRoute.Prefix.String(),
|
||||
PrefixType: route.IPv4PrefixString,
|
||||
Peer: existingPeerID,
|
||||
Masquerade: baseExistingRoute.Masquerade,
|
||||
Enabled: baseExistingRoute.Enabled,
|
||||
Metric: baseExistingRoute.Metric,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PATCH Not Found Peer",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + existingRouteID,
|
||||
requestBody: bytes.NewBufferString(fmt.Sprintf("[{\"op\":\"replace\",\"path\":\"peer\",\"value\":[\"%s\"]}]", notFoundPeerID)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "PATCH Not Found Route",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/routes/" + notFoundRouteID,
|
||||
requestBody: bytes.NewBufferString("[{\"op\":\"replace\",\"path\":\"prefix\",\"value\":[\"192.168.0.0/34\"]}]"),
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: false,
|
||||
},
|
||||
}
|
||||
|
||||
p := initRoutesTestData()
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/routes/{id}", p.GetRouteHandler).Methods("GET")
|
||||
router.HandleFunc("/api/routes/{id}", p.DeleteRouteHandler).Methods("DELETE")
|
||||
router.HandleFunc("/api/routes", p.CreateRouteHandler).Methods("POST")
|
||||
router.HandleFunc("/api/routes/{id}", p.UpdateRouteHandler).Methods("PUT")
|
||||
router.HandleFunc("/api/routes/{id}", p.PatchRouteHandler).Methods("PATCH")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
if status := recorder.Code; status != tc.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
||||
status, tc.expectedStatus, string(content))
|
||||
return
|
||||
}
|
||||
|
||||
if !tc.expectedBody {
|
||||
return
|
||||
}
|
||||
|
||||
got := &api.Route{}
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.Equal(t, got, tc.expectedRoute)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user