Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ping endpoint #25

Merged
merged 10 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ func SetAuthSecret(secret string) error {
}
}

type CustomClaims struct {
Authorized bool `json:"authorized"`
NodeId string `json:"node_id"`
jwt.StandardClaims
}

func CreateNewToken(nodeId string) (string, error) {
// set claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"authorized": true,
"node_id": nodeId,
})
claims := CustomClaims{
Authorized: true,
NodeId: nodeId,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(authSecret))
}
23 changes: 23 additions & 0 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package auth

import (
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"os"
"testing"
Expand Down Expand Up @@ -39,3 +40,25 @@ func TestSetAuthSecret(t *testing.T) {
})
}
}

func TestCreateNewToken(t *testing.T) {
tests := []struct {
name string
}{
{name: "Valid token with claims generated"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
jwtToken, err := CreateNewToken("test-node-1")
assert.NoError(t, err, "Should successfully generate token")
token, err := jwt.ParseWithClaims(jwtToken, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(authSecret), nil
})
assert.NoError(t, err, "Should successfully parse token")
claims, ok := token.Claims.(*CustomClaims)
assert.True(t, ok, "Should contain custom claims")
assert.Equal(t, "test-node-1", claims.NodeId, "Claims should have nodeId")
})
}
}

48 changes: 48 additions & 0 deletions internal/auth/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package auth

import (
"context"
"fmt"
"github.com/dgrijalva/jwt-go"
"log"
"net/http"
"time"
)

type ContextKey string

const RequestContextKey = ContextKey("request")

type RequestContext struct {
NodeId string
Timestamp time.Time
}

func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwtToken := r.Header.Get("X-Auth-Header")

token, err := jwt.ParseWithClaims(jwtToken, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(authSecret), nil
})

if err == nil {
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
c := &RequestContext{
NodeId: claims.NodeId,
Timestamp: time.Now(),
}
ctx := context.WithValue(r.Context(), RequestContextKey, c)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
}

log.Println("Unauthorized request:", err)
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Unauthorized"))
})
}
47 changes: 47 additions & 0 deletions internal/auth/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package auth

import (
"bytes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)

func TestAuthMiddleware(t *testing.T) {
validToken, _ := CreateNewToken("test-node-id")
tests := []struct {
name string
token string
status int
mockHandle http.HandlerFunc
}{
{
name: "Authorized request",
token: validToken,
status: http.StatusOK,
mockHandle: func(writer http.ResponseWriter, request *http.Request) {
r := request.Context().Value(RequestContextKey).(*RequestContext)
assert.Equal(t, r.NodeId, "test-node-id")
assert.NotNil(t, r.Timestamp)
},
},
{
name: "Unauthorized request",
token: "invalidtokenstring",
status: http.StatusUnauthorized,
mockHandle: func(writer http.ResponseWriter, request *http.Request) {},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", bytes.NewReader(nil))
req.Header.Add("X-Auth-Header", test.token)
rr := httptest.NewRecorder()

handler := AuthMiddleware(test.mockHandle)
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, test.status)
})
}
}
4 changes: 3 additions & 1 deletion internal/controllers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (

type ApiController struct {
nodeRepo models.NodeRepository
pingRepo models.PingRepository
}

func NewApiController(nodeRepo models.NodeRepository) *ApiController {
func NewApiController(nodeRepo models.NodeRepository, pingRepo models.PingRepository) *ApiController {
return &ApiController{
nodeRepo: nodeRepo,
pingRepo: pingRepo,
}
}
22 changes: 22 additions & 0 deletions internal/controllers/ping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package controllers

import (
"github.com/NodeFactoryIo/vedran/internal/auth"
"github.com/NodeFactoryIo/vedran/internal/models"
"log"
"net/http"
)

func (c ApiController) PingHandler(w http.ResponseWriter, r *http.Request) {
request := r.Context().Value(auth.RequestContextKey).(*auth.RequestContext)
err := c.pingRepo.Save(&models.Ping{
NodeId: request.NodeId,
Timestamp: request.Timestamp,
})
if err != nil {
// error on saving in database
log.Println(err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
54 changes: 54 additions & 0 deletions internal/controllers/ping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package controllers

import (
"bytes"
"context"
"fmt"
"github.com/NodeFactoryIo/vedran/internal/auth"
"github.com/NodeFactoryIo/vedran/internal/models"
mocks "github.com/NodeFactoryIo/vedran/mocks/models"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestApiController_PingHandler(t *testing.T) {
tests := []struct {
name string
}{
{name: "Valid ping request"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
timestamp := time.Now()
// create mock controller
nodeRepoMock := mocks.NodeRepository{}
pingRepoMock := mocks.PingRepository{}
pingRepoMock.On("Save", &models.Ping{
NodeId: "1",
Timestamp: timestamp,
}).Return(nil)
apiController := NewApiController(&nodeRepoMock, &pingRepoMock)
handler := http.HandlerFunc(apiController.PingHandler)

// create test request and populate context
req, _ := http.NewRequest("POST", "/api/v1/node", bytes.NewReader(nil))
c := &auth.RequestContext{
NodeId: "1",
Timestamp: timestamp,
}
ctx := context.WithValue(req.Context(), auth.RequestContextKey, c)
req = req.WithContext(ctx)
rr := httptest.NewRecorder()

// invoke test request
handler.ServeHTTP(rr, req)

// asserts
assert.Equal(t, rr.Code, http.StatusOK, fmt.Sprintf("Response status code should be %d", http.StatusOK))
assert.True(t, pingRepoMock.AssertNumberOfCalls(t, "Save", 1))
})
}
}
11 changes: 8 additions & 3 deletions internal/controllers/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"testing"
)

func TestRegisterHandler(t *testing.T) {
func TestApiController_RegisterHandler(t *testing.T) {
// define test cases
tests := []struct {
name string
Expand All @@ -39,27 +39,32 @@ func TestRegisterHandler(t *testing.T) {
// execute tests
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create mock controller
nodeRepoMock := mocks.NodeRepository{}
pingRepoMock := mocks.PingRepository{}
nodeRepoMock.On("Save", &models.Node{
ID: test.registerRequest.Id,
ConfigHash: test.registerRequest.ConfigHash,
NodeUrl: test.registerRequest.NodeUrl,
PayoutAddress: test.registerRequest.PayoutAddress,
Token: test.registerResponse.Token,
}).Return(nil)
apiController := NewApiController(&nodeRepoMock, &pingRepoMock)
handler := http.HandlerFunc(apiController.RegisterHandler)

// create test request
rb, _ := json.Marshal(test.registerRequest)
req, err := http.NewRequest("POST", "/api/v1/node", bytes.NewReader(rb))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
apiController := NewApiController(&nodeRepoMock)
handler := http.HandlerFunc(apiController.RegisterHandler)

// invoke test request
handler.ServeHTTP(rr, req)
var response RegisterResponse
_ = json.Unmarshal(rr.Body.Bytes(), &response)

// asserts
assert.Equal(t, rr.Code, test.httpStatus, fmt.Sprintf("Response status code should be %d", test.httpStatus))
assert.Equal(t, response, test.registerResponse, fmt.Sprintf("Response should be %v", test.registerResponse))
Expand Down
2 changes: 1 addition & 1 deletion internal/models/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Node struct {
}

type NodeRepository interface {
FindByID(ID int) (*Node, error)
FindByID(ID string) (*Node, error)
Save(node *Node) error
GetAll() (*[]Node, error)
}
14 changes: 14 additions & 0 deletions internal/models/ping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package models

import "time"

type Ping struct {
NodeId string `storm:"id"`
Timestamp time.Time
}

type PingRepository interface {
FindByNodeID(nodeId string) (*Ping, error)
Save(ping *Ping) error
GetAll() (*[]Ping, error)
}
2 changes: 1 addition & 1 deletion internal/repositories/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewNodeRepo(db *storm.DB) *NodeRepo {
}
}

func (r *NodeRepo) FindByID(ID int) (*models.Node, error) {
func (r *NodeRepo) FindByID(ID string) (*models.Node, error) {
var node *models.Node
err := r.db.One("ID", ID, node)
return node, err
Expand Down
32 changes: 32 additions & 0 deletions internal/repositories/ping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package repositories

import (
"github.com/NodeFactoryIo/vedran/internal/models"
"github.com/asdine/storm/v3"
)

type PingRepo struct {
db *storm.DB
}

func NewPingRepo(db *storm.DB) *PingRepo {
return &PingRepo{
db: db,
}
}

func (r *PingRepo) FindByNodeID(nodeId string) (*models.Ping, error) {
var ping *models.Ping
err := r.db.One("NodeId", nodeId, ping)
return ping, err
}

func (r *PingRepo) Save(ping *models.Ping) error {
return r.db.Save(ping)
}

func (r PingRepo) GetAll() (*[]models.Ping, error) {
var pings []models.Ping
err := r.db.All(&pings)
return &pings, err
}
20 changes: 17 additions & 3 deletions internal/router/router.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
package router

import (
"github.com/NodeFactoryIo/vedran/internal/auth"
"github.com/NodeFactoryIo/vedran/internal/controllers"
"github.com/NodeFactoryIo/vedran/internal/repositories"
"github.com/asdine/storm/v3"
"github.com/gorilla/mux"
"net/http"
)

func CreateNewApiRouter(db *storm.DB) *mux.Router {
router := mux.NewRouter()
// initialize repos
nodeRepo := repositories.NewNodeRepo(db)
pingRepo := repositories.NewPingRepo(db)
// initialize controllers
apiController := controllers.NewApiController(nodeRepo)
apiController := controllers.NewApiController(nodeRepo, pingRepo)
// map controllers handlers to endpoints
router.HandleFunc("/api/v1/nodes", apiController.RegisterHandler).Methods("POST").Name("/api/v1/nodes")

createRoute("/api/v1/nodes", "POST", apiController.RegisterHandler, router, false)
createRoute("/api/v1/nodes/pings", "POST", apiController.PingHandler, router, true)
return router
}

func createRoute(route string, method string, handler http.HandlerFunc, router *mux.Router, authorized bool) {
var r *mux.Route
if authorized {
r = router.Handle(route, auth.AuthMiddleware(handler))
} else {
r = router.Handle(route, handler)
}
r.Methods(method)
r.Name(route)
}
2 changes: 2 additions & 0 deletions internal/time/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package time

Loading