From 1bae7f13ad5f49e3104c5ca41ba1725ad6717939 Mon Sep 17 00:00:00 2001 From: claudemiro Date: Sun, 6 Mar 2016 15:34:12 -0300 Subject: [PATCH 1/5] Internal refactoring. Removing the global config. --- ipe/app.go | 18 ++++- ipe/app_test.go | 31 ++++---- ipe/auth.go | 77 -------------------- ipe/config.go | 59 +++++++-------- ipe/context.go | 22 ++++++ ipe/db.go | 58 +++++++++++++++ ipe/db_test.go | 59 +++++++++++++++ ipe/extra_handlers.go | 34 --------- ipe/{rest.go => handlers.go} | 108 +++++++++++++++++++++++++--- ipe/ipe.go | 35 +++++++-- ipe/router.go | 40 ++++++----- ipe/routes.go | 56 --------------- ipe/{websockets.go => websocket.go} | 4 +- 13 files changed, 347 insertions(+), 254 deletions(-) delete mode 100644 ipe/auth.go create mode 100644 ipe/context.go create mode 100644 ipe/db.go create mode 100644 ipe/db_test.go delete mode 100644 ipe/extra_handlers.go rename ipe/{rest.go => handlers.go} (71%) delete mode 100644 ipe/routes.go rename ipe/{websockets.go => websocket.go} (98%) diff --git a/ipe/app.go b/ipe/app.go index ffe2913..0c77a47 100644 --- a/ipe/app.go +++ b/ipe/app.go @@ -33,11 +33,25 @@ type app struct { Stats *expvar.Map `json:"-"` } -// Alloc memory for Connections and Channels -func (a *app) Init() { +func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app { + + a := &app{ + Name: name, + AppID: appID, + Key: key, + Secret: secret, + OnlySSL: onlySSL, + ApplicationDisabled: disabled, + UserEvents: userEvents, + WebHooks: webHooks, + URLWebHook: webHookURL, + } + a.Connections = make(map[string]*connection) a.Channels = make(map[string]*channel) a.Stats = expvar.NewMap(fmt.Sprintf("%s (%s)", a.Name, a.AppID)) + + return a } // Only Presence channels diff --git a/ipe/app_test.go b/ipe/app_test.go index b83e198..064dfaa 100644 --- a/ipe/app_test.go +++ b/ipe/app_test.go @@ -11,17 +11,16 @@ import ( var id = 0 -func newApp() *app { - - a := app{Name: "Test", AppID: strconv.Itoa(id), Key: "123", Secret: "123", OnlySSL: false, ApplicationDisabled: false, UserEvents: true} - a.Init() +func newTestApp() *app { + a := newApp("Test", strconv.Itoa(id), "123", "123", false, false, true, false, "") id++ - return &a + + return a } func TestConnect(t *testing.T) { - app := newApp() + app := newTestApp() app.Connect(newConnection("socketID", nil)) @@ -32,7 +31,7 @@ func TestConnect(t *testing.T) { } func TestDisconnect(t *testing.T) { - app := newApp() + app := newTestApp() app.Connect(newConnection("socketID", nil)) app.Disconnect("socketID") @@ -44,7 +43,7 @@ func TestDisconnect(t *testing.T) { } func TestFindConnection(t *testing.T) { - app := newApp() + app := newTestApp() app.Connect(newConnection("socketID", nil)) @@ -59,7 +58,7 @@ func TestFindConnection(t *testing.T) { } func TestFindChannelByChannelID(t *testing.T) { - app := newApp() + app := newTestApp() channel := newChannel("ID") app.AddChannel(channel) @@ -70,7 +69,7 @@ func TestFindChannelByChannelID(t *testing.T) { } func TestFindOrCreateChannelByChannelID(t *testing.T) { - app := newApp() + app := newTestApp() if len(app.Channels) != 0 { t.Error("Length of channels must be 0 before test") @@ -85,7 +84,7 @@ func TestFindOrCreateChannelByChannelID(t *testing.T) { } func TestRemoveChannel(t *testing.T) { - app := newApp() + app := newTestApp() if len(app.Channels) != 0 { t.Error("Length of channels must be 0 before test") @@ -108,7 +107,7 @@ func TestRemoveChannel(t *testing.T) { func Test_add_channels(t *testing.T) { - app := newApp() + app := newTestApp() // Public @@ -149,7 +148,7 @@ func Test_add_channels(t *testing.T) { } func Test_AllChannels(t *testing.T) { - app := newApp() + app := newTestApp() app.AddChannel(newChannel("private-test")) app.AddChannel(newChannel("presence-test")) app.AddChannel(newChannel("test")) @@ -160,7 +159,7 @@ func Test_AllChannels(t *testing.T) { } func Test_New_Subscriber(t *testing.T) { - app := newApp() + app := newTestApp() if len(app.Connections) != 0 { t.Error("Length of subscribers before test must be 0") @@ -175,7 +174,7 @@ func Test_New_Subscriber(t *testing.T) { } func Test_find_subscriber(t *testing.T) { - app := newApp() + app := newTestApp() conn := newConnection("1", nil) app.Connect(conn) @@ -203,7 +202,7 @@ func Test_find_subscriber(t *testing.T) { } func Test_find_or_create_channels(t *testing.T) { - app := newApp() + app := newTestApp() // Public if len(app.PublicChannels()) != 0 { diff --git a/ipe/auth.go b/ipe/auth.go deleted file mode 100644 index 66094e3..0000000 --- a/ipe/auth.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package ipe - -import ( - "net/http" - "net/url" - "sort" - "strings" - - log "github.com/golang/glog" - "github.com/gorilla/mux" - - "github.com/dimiro1/ipe/utils" -) - -// Prepare Querystring -func prepareQueryString(params url.Values) string { - var keys []string - - for key := range params { - keys = append(keys, strings.ToLower(key)) - } - - sort.Strings(keys) - - var pieces []string - - for _, key := range keys { - pieces = append(pieces, key+"="+params.Get(key)) - } - - return strings.Join(pieces, "&") -} - -// Authenticate pusher -// see: https://gist.github.com/mloughran/376898 -// -// The signature is a HMAC SHA256 hex digest. -// This is generated by signing a string made up of the following components concatenated with newline characters \n. -// -// * The uppercase request method (e.g. POST) -// * The request path (e.g. /some/resource) -// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string. -// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else) -func restAuthenticationHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - appID := vars["app_id"] - - app, err := conf.GetAppByAppID(appID) - - if err != nil { - log.Error(err) - http.Error(w, "Not authorized", http.StatusUnauthorized) - return - } - - params := r.URL.Query() - - signature := params.Get("auth_signature") - params.Del("auth_signature") - - queryString := prepareQueryString(params) - - toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString - - if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature { - h.ServeHTTP(w, r) - } else { - log.Error("Not authorized") - http.Error(w, "Not authorized", http.StatusUnauthorized) - } - }) -} diff --git a/ipe/config.go b/ipe/config.go index b0d5b14..6837761 100644 --- a/ipe/config.go +++ b/ipe/config.go @@ -1,54 +1,43 @@ -// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. +// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package ipe -import ( - "errors" - "strings" -) - // The config file type configFile struct { Host string // The host, eg: :8080 will start on 0.0.0.0:8080 User string - Password string SSL bool SSLHost string SSLKeyFile string SSLCertFile string - Apps []*app + Apps []configApp } -// Initialize Apps -func (c *configFile) Init() { - for _, app := range c.Apps { - app.Init() - } +type configApp struct { + Name string + AppID string + Key string + Secret string + OnlySSL bool + ApplicationDisabled bool + UserEvents bool + WebHooks bool + URLWebHook string } -func (c *configFile) WasProvidedUserAndPassword() bool { - return len(strings.TrimSpace(c.User)) > 0 && len(strings.TrimSpace(c.Password)) > 0 -} - -// Returns an App with by appID -func (c *configFile) GetAppByAppID(appID string) (*app, error) { - for _, a := range c.Apps { - if a.AppID == appID { - return a, nil - } - } - return &app{}, errors.New("App not found") -} - -// Returns an App with by key -func (c *configFile) GetAppByKey(key string) (*app, error) { - for _, a := range c.Apps { - if a.Key == key { - return a, nil - } - } - return &app{}, errors.New("App not found") +func newAppFromConfig(a configApp) *app { + return newApp( + a.Name, + a.AppID, + a.Key, + a.Secret, + a.OnlySSL, + a.ApplicationDisabled, + a.UserEvents, + a.WebHooks, + a.URLWebHook, + ) } diff --git a/ipe/context.go b/ipe/context.go new file mode 100644 index 0000000..0ca02ab --- /dev/null +++ b/ipe/context.go @@ -0,0 +1,22 @@ +// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package ipe + +import "net/http" + +type applicationContext struct { + DB db +} + +// A handlerHTTPC responds to an HTTP request with custom application context. +type handlerHTTPC interface { + ServeHTTPC(ctx *applicationContext, w http.ResponseWriter, r *http.Request) +} + +type handlerHTTPCFunc func(ctx *applicationContext, w http.ResponseWriter, r *http.Request) + +func (c handlerHTTPCFunc) ServeHTTPC(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { + c(ctx, w, r) +} diff --git a/ipe/db.go b/ipe/db.go new file mode 100644 index 0000000..ddf1db5 --- /dev/null +++ b/ipe/db.go @@ -0,0 +1,58 @@ +// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package ipe + +import ( + "errors" + "sync" +) + +// db represents a app database +// For now it there is only one memory database implementation +// but in the future I can write a sql implementation +type db interface { + GetAppByAppID(appID string) (*app, error) + GetAppByKey(key string) (*app, error) + AddApp(*app) error +} + +// memdb is a in memory implementation of db interface +type memdb struct { + sync.Mutex + Apps []*app +} + +func newMemdb() *memdb { + return &memdb{} +} + +func (db *memdb) AddApp(a *app) error { + db.Lock() + defer db.Unlock() + + db.Apps = append(db.Apps, a) + + return nil +} + +// GetAppByAppID returns an App with by appID +func (db *memdb) GetAppByAppID(appID string) (*app, error) { + for _, a := range db.Apps { + if a.AppID == appID { + return a, nil + } + } + return nil, errors.New("App not found") +} + +// GetAppByKey returns an App with by key +func (db *memdb) GetAppByKey(key string) (*app, error) { + for _, a := range db.Apps { + if a.Key == key { + return a, nil + } + } + return nil, errors.New("App not found") +} diff --git a/ipe/db_test.go b/ipe/db_test.go new file mode 100644 index 0000000..3ee0012 --- /dev/null +++ b/ipe/db_test.go @@ -0,0 +1,59 @@ +// Copyright 2016 Claudemiro Alves Feitosa Neto. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package ipe + +import "testing" + +func Test_db_GetAppByAppID(t *testing.T) { + app := &app{AppID: "123456", Name: "Example"} + + db := newMemdb() + db.AddApp(app) + + a, err := db.GetAppByAppID("123456") + + if err != nil { + t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app) + } +} + +func Test_db_GetAppByAppID__error(t *testing.T) { + app := &app{AppID: "123456", Name: "Example"} + + db := newMemdb() + db.AddApp(app) + + a, err := db.GetAppByAppID("not-found") + + if err == nil { + t.Errorf("GetAppByAppID(%q) == %q, want %q", "123456", a, app) + } +} + +func Test_db_GetAppByKey(t *testing.T) { + app := &app{AppID: "123456", Name: "Example", Key: "654321"} + + db := newMemdb() + db.AddApp(app) + + a, err := db.GetAppByKey("654321") + + if err != nil { + t.Errorf("GetAppByKey(%q) == %q, want %q", "654321", a, app) + } +} + +func Test_db_GetAppByKey__error(t *testing.T) { + app := &app{AppID: "123456", Name: "Example", Key: "654321"} + + db := newMemdb() + db.AddApp(app) + + a, err := db.GetAppByKey("not-found") + + if err == nil { + t.Errorf("GetAppByKey(%q) == %q, want %v", "not-found", a, nil) + } +} diff --git a/ipe/extra_handlers.go b/ipe/extra_handlers.go deleted file mode 100644 index 4bf1b5c..0000000 --- a/ipe/extra_handlers.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package ipe - -import ( - "fmt" - "net/http" - - "github.com/gorilla/mux" -) - -// Check if the application is disabled -func restCheckAppDisabledHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - appID := vars["app_id"] - - currentApp, err := conf.GetAppByAppID(appID) - - if err != nil { - http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden) - return - } - - if currentApp.ApplicationDisabled { - http.Error(w, "Application disabled", http.StatusForbidden) - return - } - - h.ServeHTTP(w, r) - }) -} diff --git a/ipe/rest.go b/ipe/handlers.go similarity index 71% rename from ipe/rest.go rename to ipe/handlers.go index e10b8af..d18945a 100644 --- a/ipe/rest.go +++ b/ipe/handlers.go @@ -8,13 +8,103 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" + "sort" "strings" - "github.com/dimiro1/ipe/utils" log "github.com/golang/glog" "github.com/gorilla/mux" + + "github.com/dimiro1/ipe/utils" ) +// Prepare Querystring +func prepareQueryString(params url.Values) string { + var keys []string + + for key := range params { + keys = append(keys, strings.ToLower(key)) + } + + sort.Strings(keys) + + var pieces []string + + for _, key := range keys { + pieces = append(pieces, key+"="+params.Get(key)) + } + + return strings.Join(pieces, "&") +} + +// Authenticate pusher +// see: https://gist.github.com/mloughran/376898 +// +// The signature is a HMAC SHA256 hex digest. +// This is generated by signing a string made up of the following components concatenated with newline characters \n. +// +// * The uppercase request method (e.g. POST) +// * The request path (e.g. /some/resource) +// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string. +// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else) +func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerHTTPC { + return handlerHTTPCFunc(func(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + appID := vars["app_id"] + + app, err := ctx.DB.GetAppByAppID(appID) + + if err != nil { + log.Error(err) + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + params := r.URL.Query() + + signature := params.Get("auth_signature") + params.Del("auth_signature") + + queryString := prepareQueryString(params) + + toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString + + if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature { + h.ServeHTTPC(ctx, w, r) + } else { + log.Error("Not authorized") + http.Error(w, "Not authorized", http.StatusUnauthorized) + } + }) +} + +// Check if the application is disabled +func restCheckAppDisabledHandler(ctx *applicationContext, h handlerHTTPC) handlerHTTPC { + return handlerHTTPCFunc(func(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + appID := vars["app_id"] + + currentApp, err := ctx.DB.GetAppByAppID(appID) + + if err != nil { + http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden) + return + } + + if currentApp.ApplicationDisabled { + http.Error(w, "Application disabled", http.StatusForbidden) + return + } + + h.ServeHTTPC(ctx, w, r) + }) +} + +// commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers +func commonHandlers(ctx *applicationContext, h handlerHTTPCFunc) handlerHTTPC { + return restCheckAppDisabledHandler(ctx, restAuthenticationHandler(ctx, h)) +} + // An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels. // This is conventionally known as triggering an event. // @@ -30,11 +120,11 @@ import ( // Response is an empty JSON hash. // // POST /apps/{app_id}/events -func postEvents(w http.ResponseWriter, r *http.Request) { +func postEvents(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) appID := vars["app_id"] - app, err := conf.GetAppByAppID(appID) + app, err := ctx.DB.GetAppByAppID(appID) if err != nil { http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) @@ -96,7 +186,7 @@ func postEvents(w http.ResponseWriter, r *http.Request) { // } // // GET /apps/{app_id}/channels -func getChannels(w http.ResponseWriter, r *http.Request) { +func getChannels(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { params := r.URL.Query() vars := mux.Vars(r) @@ -121,7 +211,7 @@ func getChannels(w http.ResponseWriter, r *http.Request) { return } - app, err := conf.GetAppByAppID(appID) + app, err := ctx.DB.GetAppByAppID(appID) if err != nil { http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) @@ -177,14 +267,14 @@ func getChannels(w http.ResponseWriter, r *http.Request) { // } // // GET /apps/{app_id}/channels/{channel_name} -func getChannel(w http.ResponseWriter, r *http.Request) { +func getChannel(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") params := r.URL.Query() vars := mux.Vars(r) appID := vars["app_id"] - app, err := conf.GetAppByAppID(appID) + app, err := ctx.DB.GetAppByAppID(appID) if err != nil { http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) @@ -267,7 +357,7 @@ func getChannel(w http.ResponseWriter, r *http.Request) { // } // // GET /apps/{app_id}/channels/{channel_name}/users -func getChannelUsers(w http.ResponseWriter, r *http.Request) { +func getChannelUsers(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) appID := vars["app_id"] @@ -280,7 +370,7 @@ func getChannelUsers(w http.ResponseWriter, r *http.Request) { return } - app, err := conf.GetAppByAppID(appID) + app, err := ctx.DB.GetAppByAppID(appID) if err != nil { http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) diff --git a/ipe/ipe.go b/ipe/ipe.go index 1c9ca3b..bd710d6 100644 --- a/ipe/ipe.go +++ b/ipe/ipe.go @@ -14,25 +14,46 @@ import ( log "github.com/golang/glog" ) -// Conf holds the global configuration state -var conf configFile - // Start Parse the configuration file and starts the ipe server // It Panic if could not start the HTTP or HTTPS server -func Start(configfile string) { +func Start(filename string) { + var conf configFile + rand.Seed(time.Now().Unix()) - file, err := os.Open(configfile) + file, err := os.Open(filename) if err != nil { log.Fatal(err) } + // Reading config if err := json.NewDecoder(file).Decode(&conf); err != nil { log.Fatal(err) } - conf.Init() - router := newRouter() + // Using a in memory database + db := newMemdb() + + // Adding applications + for _, a := range conf.Apps { + db.AddApp(newAppFromConfig(a)) + } + + // Creating the global application context + ctx := &applicationContext{DB: db} + + // The router + router := newRouter(ctx) + + router.POST("/apps/{app_id}/events", commonHandlers(ctx, postEvents)) + + router.GET("/apps/{app_id}/channels", commonHandlers(ctx, getChannels)) + + router.GET("/apps/{app_id}/channels/{channel_name}", commonHandlers(ctx, getChannel)) + + router.GET("/apps/{app_id}/channels/{channel_name}/users", commonHandlers(ctx, getChannelUsers)) + + router.GET("/app/{key}", handlerHTTPCFunc(wsHandler)) if conf.SSL { go func() { diff --git a/ipe/router.go b/ipe/router.go index dc0b8ab..e553501 100644 --- a/ipe/router.go +++ b/ipe/router.go @@ -1,4 +1,4 @@ -// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. +// Copyright 2014, 2016 Claudemiro Alves Feitosa Neto. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -10,23 +10,31 @@ import ( "github.com/gorilla/mux" ) -// newRouter is a function that returns a new configured Router -// It add the necessary middlewares -func newRouter() *mux.Router { - router := mux.NewRouter().StrictSlash(true) +type router struct { + ctx *applicationContext + mux *mux.Router + routes map[string]handlerHTTPC +} - for _, route := range routes { - var handler http.Handler - - handler = route.HandlerFunc +func newRouter(ctx *applicationContext) *router { + return &router{ + ctx: ctx, + mux: mux.NewRouter().StrictSlash(true), + } +} - if route.RequiresRestAuth { - handler = restAuthenticationHandler(handler) - handler = restCheckAppDisabledHandler(handler) - } +func (a *router) GET(path string, handler handlerHTTPC) { + a.mux.Methods("GET").Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler.ServeHTTPC(a.ctx, w, r) + }) +} - router.Methods(route.Method).Path(route.Pattern).Name(route.Name).Handler(handler) - } +func (a *router) POST(path string, handler handlerHTTPC) { + a.mux.Methods("POST").Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler.ServeHTTPC(a.ctx, w, r) + }) +} - return router +func (a router) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.mux.ServeHTTP(w, r) } diff --git a/ipe/routes.go b/ipe/routes.go deleted file mode 100644 index e1cbbf5..0000000 --- a/ipe/routes.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package ipe - -import ( - "net/http" -) - -// A route -type route struct { - Name string - Method string - Pattern string - HandlerFunc http.HandlerFunc - RequiresRestAuth bool -} - -var routes = []route{ - { - "PostEvents", - "POST", - "/apps/{app_id}/events", - postEvents, - true, - }, - { - "GetChannels", - "GET", - "/apps/{app_id}/channels", - getChannels, - true, - }, - { - "GetChannel", - "GET", - "/apps/{app_id}/channels/{channel_name}", - getChannel, - true, - }, - { - "GetChannelUsers", - "GET", - "/apps/{app_id}/channels/{channel_name}/users", - getChannelUsers, - true, - }, - { - "Websocket", - "GET", - "/app/{key}", - wsHandler, - false, - }, -} diff --git a/ipe/websockets.go b/ipe/websocket.go similarity index 98% rename from ipe/websockets.go rename to ipe/websocket.go index a63249e..5d57f33 100644 --- a/ipe/websockets.go +++ b/ipe/websocket.go @@ -207,7 +207,7 @@ func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, ses } // Websocket GET /app/{key} -func wsHandler(w http.ResponseWriter, r *http.Request) { +func wsHandler(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) defer func() { if conn != nil { @@ -223,7 +223,7 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) appKey := vars["key"] - app, err := conf.GetAppByKey(appKey) + app, err := ctx.DB.GetAppByKey(appKey) if err != nil { log.Error(err) From 383c02de1c2c0dab9a0ae17cc61913c7b5e2a877 Mon Sep 17 00:00:00 2001 From: claudemiro Date: Sun, 6 Mar 2016 16:09:21 -0300 Subject: [PATCH 2/5] Added params as a parameter for each handler. --- functional/Procfile | 2 +- ipe/context.go | 15 ++++++++---- ipe/handlers.go | 58 +++++++++++++++++++-------------------------- ipe/router.go | 14 +++++++---- ipe/websocket.go | 6 ++--- 5 files changed, 48 insertions(+), 47 deletions(-) diff --git a/functional/Procfile b/functional/Procfile index 61ac41f..807bb49 100644 --- a/functional/Procfile +++ b/functional/Procfile @@ -1,2 +1,2 @@ client: go run client.go -server: go run ../main.go -config ./functional-config.json -logtostderr \ No newline at end of file +server: go run ../main.go -config ./functional-config.json -alsologtostderr \ No newline at end of file diff --git a/ipe/context.go b/ipe/context.go index 0ca02ab..bd98b0d 100644 --- a/ipe/context.go +++ b/ipe/context.go @@ -10,13 +10,20 @@ type applicationContext struct { DB db } +// url params +type params map[string]string + +func (p params) Get(key string) string { + return p[key] +} + // A handlerHTTPC responds to an HTTP request with custom application context. type handlerHTTPC interface { - ServeHTTPC(ctx *applicationContext, w http.ResponseWriter, r *http.Request) + ServeHTTPC(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) } -type handlerHTTPCFunc func(ctx *applicationContext, w http.ResponseWriter, r *http.Request) +type handlerHTTPCFunc func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) -func (c handlerHTTPCFunc) ServeHTTPC(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { - c(ctx, w, r) +func (c handlerHTTPCFunc) ServeHTTPC(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { + c(ctx, p, w, r) } diff --git a/ipe/handlers.go b/ipe/handlers.go index d18945a..b279e95 100644 --- a/ipe/handlers.go +++ b/ipe/handlers.go @@ -13,7 +13,6 @@ import ( "strings" log "github.com/golang/glog" - "github.com/gorilla/mux" "github.com/dimiro1/ipe/utils" ) @@ -48,9 +47,8 @@ func prepareQueryString(params url.Values) string { // * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string. // Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else) func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerHTTPC { - return handlerHTTPCFunc(func(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - appID := vars["app_id"] + return handlerHTTPCFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { + appID := p.Get("app_id") app, err := ctx.DB.GetAppByAppID(appID) @@ -60,17 +58,17 @@ func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerH return } - params := r.URL.Query() + query := r.URL.Query() - signature := params.Get("auth_signature") - params.Del("auth_signature") + signature := query.Get("auth_signature") + query.Del("auth_signature") - queryString := prepareQueryString(params) + queryString := prepareQueryString(query) toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature { - h.ServeHTTPC(ctx, w, r) + h.ServeHTTPC(ctx, p, w, r) } else { log.Error("Not authorized") http.Error(w, "Not authorized", http.StatusUnauthorized) @@ -80,9 +78,8 @@ func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerH // Check if the application is disabled func restCheckAppDisabledHandler(ctx *applicationContext, h handlerHTTPC) handlerHTTPC { - return handlerHTTPCFunc(func(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - appID := vars["app_id"] + return handlerHTTPCFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { + appID := p.Get("app_id") currentApp, err := ctx.DB.GetAppByAppID(appID) @@ -96,7 +93,7 @@ func restCheckAppDisabledHandler(ctx *applicationContext, h handlerHTTPC) handle return } - h.ServeHTTPC(ctx, w, r) + h.ServeHTTPC(ctx, p, w, r) }) } @@ -120,9 +117,8 @@ func commonHandlers(ctx *applicationContext, h handlerHTTPCFunc) handlerHTTPC { // Response is an empty JSON hash. // // POST /apps/{app_id}/events -func postEvents(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - appID := vars["app_id"] +func postEvents(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { + appID := p.Get("app_id") app, err := ctx.DB.GetAppByAppID(appID) @@ -186,13 +182,12 @@ func postEvents(ctx *applicationContext, w http.ResponseWriter, r *http.Request) // } // // GET /apps/{app_id}/channels -func getChannels(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - vars := mux.Vars(r) +func getChannels(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() - appID := vars["app_id"] - filter := params.Get("filter_by_prefix") - info := params.Get("info") + appID := p.Get("app_id") + filter := query.Get("filter_by_prefix") + info := query.Get("info") attributes := strings.Split(info, ",") @@ -267,20 +262,19 @@ func getChannels(ctx *applicationContext, w http.ResponseWriter, r *http.Request // } // // GET /apps/{app_id}/channels/{channel_name} -func getChannel(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { +func getChannel(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - params := r.URL.Query() - vars := mux.Vars(r) + query := r.URL.Query() - appID := vars["app_id"] + appID := p.Get("app_id") app, err := ctx.DB.GetAppByAppID(appID) if err != nil { http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest) } - channelName := vars["channel_name"] + channelName := p.Get("channel_name") // Channel name could not be empty if strings.TrimSpace(channelName) == "" { @@ -288,7 +282,7 @@ func getChannel(ctx *applicationContext, w http.ResponseWriter, r *http.Request) return } - info := params.Get("info") + info := query.Get("info") attributes := strings.Split(info, ",") // Attributes requested @@ -357,11 +351,9 @@ func getChannel(ctx *applicationContext, w http.ResponseWriter, r *http.Request) // } // // GET /apps/{app_id}/channels/{channel_name}/users -func getChannelUsers(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - appID := vars["app_id"] - channelName := vars["channel_name"] +func getChannelUsers(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { + appID := p.Get("app_id") + channelName := p.Get("channel_name") isPresence := utils.IsPresenceChannel(channelName) diff --git a/ipe/router.go b/ipe/router.go index e553501..d39d4a3 100644 --- a/ipe/router.go +++ b/ipe/router.go @@ -24,14 +24,18 @@ func newRouter(ctx *applicationContext) *router { } func (a *router) GET(path string, handler handlerHTTPC) { - a.mux.Methods("GET").Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTPC(a.ctx, w, r) - }) + a.Handle("GET", path, handler) } func (a *router) POST(path string, handler handlerHTTPC) { - a.mux.Methods("POST").Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTPC(a.ctx, w, r) + a.Handle("POST", path, handler) +} + +func (a *router) Handle(method, path string, handler handlerHTTPC) { + a.mux.Methods(method).Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p := params(mux.Vars(r)) + + handler.ServeHTTPC(a.ctx, p, w, r) }) } diff --git a/ipe/websocket.go b/ipe/websocket.go index 5d57f33..4d8d429 100644 --- a/ipe/websocket.go +++ b/ipe/websocket.go @@ -13,7 +13,6 @@ import ( "strings" log "github.com/golang/glog" - "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/dimiro1/ipe/utils" @@ -207,7 +206,7 @@ func onMessage(conn *websocket.Conn, w http.ResponseWriter, r *http.Request, ses } // Websocket GET /app/{key} -func wsHandler(ctx *applicationContext, w http.ResponseWriter, r *http.Request) { +func wsHandler(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) defer func() { if conn != nil { @@ -220,8 +219,7 @@ func wsHandler(ctx *applicationContext, w http.ResponseWriter, r *http.Request) return } - vars := mux.Vars(r) - appKey := vars["key"] + appKey := p.Get("key") app, err := ctx.DB.GetAppByKey(appKey) From a7c58135012b38b4d23bd3d737d0bbee1caef797 Mon Sep 17 00:00:00 2001 From: claudemiro Date: Sun, 6 Mar 2016 17:53:51 -0300 Subject: [PATCH 3/5] Testing http handlers --- ipe/connection.go | 5 ++ ipe/handlers_test.go | 207 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 ipe/handlers_test.go diff --git a/ipe/connection.go b/ipe/connection.go index 01a03b0..14876a0 100644 --- a/ipe/connection.go +++ b/ipe/connection.go @@ -28,6 +28,11 @@ func newConnection(socketID string, s *websocket.Conn) *connection { // Publish the message to websocket atached to this client func (conn *connection) Publish(m interface{}) { go func() { + if conn.Socket == nil { + log.Info("Socket is nil. Maybe you are testing this app?") + return + } + if err := conn.Socket.WriteJSON(m); err != nil { log.Errorf("Error publishing message to connection %+v, %s", conn, err) } diff --git a/ipe/handlers_test.go b/ipe/handlers_test.go new file mode 100644 index 0000000..8f3af12 --- /dev/null +++ b/ipe/handlers_test.go @@ -0,0 +1,207 @@ +package ipe + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +var ( + testApp *app + ctx *applicationContext +) + +func init() { + testApp = newTestApp() + + channel := newChannel("presence-c1") + testApp.AddChannel(channel) + testApp.AddChannel(newChannel("c2")) + testApp.AddChannel(newChannel("private-c3")) + + conn := newConnection("123.456", nil) + testApp.Subscribe(channel, conn, "{}") + + conn = newConnection("321.654", nil) + testApp.Subscribe(channel, conn, "{}") + + db := newMemdb() + db.AddApp(testApp) + + ctx = &applicationContext{DB: db} +} + +// All Channels +func Test_getChannels_all(t *testing.T) { + + appID := testApp.AppID + + p := map[string]string{} + p["app_id"] = appID + + r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels", appID), nil) + w := httptest.NewRecorder() + + getChannels(ctx, params(p), w, r) + + if w.Code != http.StatusOK { + t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK) + } + + data := make(map[string]interface{}) + json.Unmarshal(w.Body.Bytes(), &data) + + channels := data["channels"].(map[string]interface{}) + + if len(channels) != 3 { + t.Errorf("len(%q) == %d, want %d", channels, len(channels), 3) + } +} + +func Test_getChannels_filter_by_presence_prefix(t *testing.T) { + appID := testApp.AppID + + p := map[string]string{} + p["app_id"] = appID + + r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-", appID), nil) + w := httptest.NewRecorder() + + getChannels(ctx, params(p), w, r) + + if w.Code != http.StatusOK { + t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK) + } + + data := make(map[string]interface{}) + json.Unmarshal(w.Body.Bytes(), &data) + + channels := data["channels"].(map[string]interface{}) + + if len(channels) != 1 { + t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1) + } +} + +// Only presence channels and user_count +func Test_getChannels_filter_by_presence_prefix_and_user_count(t *testing.T) { + + appID := testApp.AppID + + p := map[string]string{} + p["app_id"] = appID + + r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=presence-&info=user_count", appID), nil) + w := httptest.NewRecorder() + + getChannels(ctx, params(p), w, r) + + if w.Code != http.StatusOK { + t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK) + } + + data := make(map[string]interface{}) + json.Unmarshal(w.Body.Bytes(), &data) + + channels := data["channels"].(map[string]interface{}) + + if len(channels) != 1 { + t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1) + } + + c, exists := channels["presence-c1"] + + if !exists { + t.Errorf("!exists == %t, want %t", !exists, false) + } + + _channel := c.(map[string]interface{}) + + if _channel["user_count"] != float64(1) { + t.Errorf("_channel['user_count'] == %f, want %d", _channel["user_count"], 1) + } +} + +// User count only alowed in Presence channels +func Test_getChannels_filter_by_private_prefix_and_info_user_count(t *testing.T) { + appID := testApp.AppID + + p := map[string]string{} + p["app_id"] = appID + + r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-&info=user_count", appID), nil) + w := httptest.NewRecorder() + + getChannels(ctx, params(p), w, r) + + if w.Code != http.StatusBadRequest { + t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusBadRequest) + } +} + +func Test_getChannels_filter_by_public_prefix(t *testing.T) { + appID := testApp.AppID + + p := map[string]string{} + p["app_id"] = appID + + r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=public-", appID), nil) + w := httptest.NewRecorder() + + getChannels(ctx, params(p), w, r) + + if w.Code != http.StatusOK { + t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK) + } + + data := make(map[string]interface{}) + + json.Unmarshal(w.Body.Bytes(), &data) + + channels := data["channels"].(map[string]interface{}) + + if len(channels) != 1 { + t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1) + } + + _, exists := channels["c2"] + + if !exists { + t.Errorf("!exists == %t, want %t", !exists, false) + } +} + +func Test_getChannels_filter_by_private_prefix(t *testing.T) { + + appID := testApp.AppID + + p := map[string]string{} + p["app_id"] = appID + + r, _ := http.NewRequest("GET", fmt.Sprintf("/apps/%s/channels?filter_by_prefix=private-", appID), nil) + w := httptest.NewRecorder() + + getChannels(ctx, params(p), w, r) + + if w.Code != http.StatusOK { + t.Errorf("w.Code == %d, wants %d", w.Code, http.StatusOK) + } + + data := make(map[string]interface{}) + + json.Unmarshal(w.Body.Bytes(), &data) + + channels := data["channels"].(map[string]interface{}) + + if len(channels) != 1 { + t.Errorf("len(%q) == %d, want %d", channels, len(channels), 1) + } + + _, exists := channels["private-c3"] + + if !exists { + t.Errorf("!exists == %t, want %t", !exists, false) + } +} From e71294de1822cb7c0fbe731e223afd1ace62bd2c Mon Sep 17 00:00:00 2001 From: claudemiro Date: Sun, 6 Mar 2016 18:12:03 -0300 Subject: [PATCH 4/5] Standardized error messages --- ipe/app_test.go | 62 +++++++++++++++++++++--------------------- ipe/channel_test.go | 24 ++++++++-------- ipe/connection_test.go | 6 ++-- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/ipe/app_test.go b/ipe/app_test.go index 064dfaa..2eeefcc 100644 --- a/ipe/app_test.go +++ b/ipe/app_test.go @@ -25,7 +25,7 @@ func TestConnect(t *testing.T) { app.Connect(newConnection("socketID", nil)) if len(app.Connections) != 1 { - t.Errorf("Connections must be 1, but was %d", len(app.Connections)) + t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1) } } @@ -37,7 +37,7 @@ func TestDisconnect(t *testing.T) { app.Disconnect("socketID") if len(app.Connections) != 0 { - t.Errorf("Connections must be 0, but was %d", len(app.Connections)) + t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0) } } @@ -48,11 +48,11 @@ func TestFindConnection(t *testing.T) { app.Connect(newConnection("socketID", nil)) if _, err := app.FindConnection("socketID"); err != nil { - t.Error("Must find Connection") + t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil) } if _, err := app.FindConnection("NotFound"); err == nil { - t.Error("Must not found Connection") + t.Errorf("app.FindConnection('socketID') == _, %q, wants !nil", err) } } @@ -64,7 +64,7 @@ func TestFindChannelByChannelID(t *testing.T) { app.AddChannel(channel) if _, err := app.FindChannelByChannelID("ID"); err != nil { - t.Error("Channel not found") + t.Errorf("app.FindChannelByChannelID('ID') == _, %q, wants %v", err, nil) } } @@ -72,13 +72,13 @@ func TestFindOrCreateChannelByChannelID(t *testing.T) { app := newTestApp() if len(app.Channels) != 0 { - t.Error("Length of channels must be 0 before test") + t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0) } app.FindOrCreateChannelByChannelID("ID") if len(app.Channels) != 1 { - t.Error("Length of channels must be 1 after test") + t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1) } } @@ -87,20 +87,20 @@ func TestRemoveChannel(t *testing.T) { app := newTestApp() if len(app.Channels) != 0 { - t.Error("Length of channels must be 0 before test") + t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0) } channel := newChannel("ID") app.AddChannel(channel) if len(app.Channels) != 1 { - t.Error("Length of channels after insert must be 1") + t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 1) } app.RemoveChannel(channel) if len(app.Channels) != 0 { - t.Error("Length of channels must be 0 after remove") + t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 0) } } @@ -112,37 +112,37 @@ func Test_add_channels(t *testing.T) { // Public if len(app.PublicChannels()) != 0 { - t.Error("Length of public channels must be 0 before test") + t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0) } app.AddChannel(newChannel("ID")) if len(app.PublicChannels()) != 1 { - t.Error("Length os public channels after insert must be 1") + t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1) } // Presence if len(app.PresenceChannels()) != 0 { - t.Error("Length of presence channels must be 0 before test") + t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0) } app.AddChannel(newChannel("presence-test")) if len(app.PresenceChannels()) != 1 { - t.Error("Length os presence channels after insert must be 1") + t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1) } // Private if len(app.PrivateChannels()) != 0 { - t.Error("Length of private channels must be 0 before test") + t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0) } app.AddChannel(newChannel("private-test")) if len(app.PrivateChannels()) != 1 { - t.Error("Length os private channels after insert must be 1") + t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1) } } @@ -154,7 +154,7 @@ func Test_AllChannels(t *testing.T) { app.AddChannel(newChannel("test")) if len(app.Channels) != 3 { - t.Error("Must have 3 channels") + t.Errorf("len(app.Channels) == %d, wants %d", len(app.Channels), 3) } } @@ -162,14 +162,14 @@ func Test_New_Subscriber(t *testing.T) { app := newTestApp() if len(app.Connections) != 0 { - t.Error("Length of subscribers before test must be 0") + t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0) } conn := newConnection("1", nil) app.Connect(conn) if len(app.Connections) != 1 { - t.Error("Length os subscribers after test must be 1") + t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1) } } @@ -185,7 +185,7 @@ func Test_find_subscriber(t *testing.T) { } if conn.SocketID != "1" { - t.Error("Wrong subscriber.") + t.Errorf("conn.SocketID == %s, wants %s", conn.SocketID, "1") } // Find a wrong subscriber @@ -193,11 +193,11 @@ func Test_find_subscriber(t *testing.T) { conn, err = app.FindConnection("DoesNotExists") if err == nil { - t.Error("Opps, Must be nil") + t.Errorf("err == %q, wants !nil", err) } if conn != nil { - t.Error("Opps, Must be nil") + t.Errorf("conn == %q, wants nil", conn) } } @@ -206,47 +206,47 @@ func Test_find_or_create_channels(t *testing.T) { // Public if len(app.PublicChannels()) != 0 { - t.Error("Length of public channels must be 0 before test") + t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 0) } c := app.FindOrCreateChannelByChannelID("id") if len(app.PublicChannels()) != 1 { - t.Error("Length os public channels after insert must be 1") + t.Errorf("len(app.PublicChannels()) == %d, wants %d", len(app.PublicChannels()), 1) } if c.ChannelID != "id" { - t.Error("Opps wrong channel") + t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "id") } // Presence if len(app.PresenceChannels()) != 0 { - t.Error("Length of presence channels must be 0 before test") + t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 0) } c = app.FindOrCreateChannelByChannelID("presence-test") if len(app.PresenceChannels()) != 1 { - t.Error("Length os presence channels after insert must be 1") + t.Errorf("len(app.PresenceChannels()) == %d, wants %d", len(app.PresenceChannels()), 1) } if c.ChannelID != "presence-test" { - t.Error("Opps wrong channel") + t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "presence-test") } // Private if len(app.PrivateChannels()) != 0 { - t.Error("Length of private channels must be 0 before test") + t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 0) } c = app.FindOrCreateChannelByChannelID("private-test") if len(app.PrivateChannels()) != 1 { - t.Error("Length os private channels after insert must be 1") + t.Errorf("len(app.PrivateChannels()) == %d, wants %d", len(app.PrivateChannels()), 1) } if c.ChannelID != "private-test" { - t.Error("Opps wrong channel") + t.Errorf("c.ChannelID == %s, wants %s", c.ChannelID, "private-test") } } diff --git a/ipe/channel_test.go b/ipe/channel_test.go index 974d702..c42b09e 100644 --- a/ipe/channel_test.go +++ b/ipe/channel_test.go @@ -10,13 +10,13 @@ func TestIsOccupied(t *testing.T) { c := newChannel("ID") if c.IsOccupied() { - t.Error("Channels must be empty") + t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false) } c.Subscriptions["ID"] = newSubscription(newConnection("ID", nil), "") if !c.IsOccupied() { - t.Error("Channels must be empty") + t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true) } } @@ -24,7 +24,7 @@ func TestIsPrivate(t *testing.T) { c := newChannel("private-channel") if !c.IsPrivate() { - t.Error("The Channel must be private") + t.Errorf("c.IsPrivate() == %t, wants %t", c.IsPrivate(), true) } } @@ -32,7 +32,7 @@ func TestIsPresence(t *testing.T) { c := newChannel("presence-channel") if !c.IsPresence() { - t.Error("The Channel must be presence") + t.Errorf("c.IsPresence() == %t, wants %t", c.IsPresence(), true) } } @@ -40,7 +40,7 @@ func TestIsPublic(t *testing.T) { c := newChannel("channel") if !c.IsPublic() { - t.Error("The Channel must be public") + t.Errorf("c.IsPublic() == %t, wants %t", c.IsPublic(), true) } } @@ -48,13 +48,13 @@ func TestIsPrivateOrPresence(t *testing.T) { c := newChannel("private-channel") if !c.IsPresenceOrPrivate() { - t.Error("The Channel must be private or presence") + t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true) } c = newChannel("presence-channel") if !c.IsPresenceOrPrivate() { - t.Error("The Channel must be private or presence") + t.Errorf("c.IsPresenceOrPrivate() == %t, wants %t", c.IsPresenceOrPrivate(), true) } } @@ -62,7 +62,7 @@ func TestTotalSubscriptions(t *testing.T) { c := newChannel("ID") if c.TotalSubscriptions() != len(c.Subscriptions) { - t.Error("TotalSubscriptions must be equal to len of total subscriptions") + t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions)) } } @@ -73,11 +73,11 @@ func TestTotalUsers(t *testing.T) { c.Subscriptions["2"] = newSubscription(newConnection("ID", nil), "") if c.TotalSubscriptions() != len(c.Subscriptions) { - t.Error("TotalSubscriptions must be equal to len of total subscriptions") + t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions)) } if c.TotalUsers() != 1 { - t.Error("TotalUsers must be equal to 1") + t.Errorf("c.TotalUsers() == %d, wants %d", c.TotalUsers(), 1) } } @@ -87,12 +87,12 @@ func TestIsSubscribed(t *testing.T) { conn := newConnection("ID", nil) if c.IsSubscribed(conn) { - t.Error("Must not be subscribed") + t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false) } c.Subscriptions["ID"] = newSubscription(conn, "") if !c.IsSubscribed(conn) { - t.Error("Must be subscribed") + t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), true) } } diff --git a/ipe/connection_test.go b/ipe/connection_test.go index 30ae7d2..34e7915 100644 --- a/ipe/connection_test.go +++ b/ipe/connection_test.go @@ -17,14 +17,14 @@ func TestNewConnection(t *testing.T) { c := newConnection(expectedSocketID, expectedSocket) if c.SocketID != expectedSocketID { - t.Errorf("Expected: %s but got %s", expectedSocketID, c.SocketID) + t.Errorf("c.SocketID == %s, wants %s", c.SocketID, expectedSocketID) } if c.Socket != expectedSocket { - t.Errorf("Expected: %+v but got %+v", expectedSocket, c.Socket) + t.Errorf("c.Socket == %v, wants %v", c.Socket, expectedSocket) } if c.CreatedAt.IsZero() { - t.Errorf("Expected %s to not be zero", c.CreatedAt) + t.Errorf("c.CreatedAt.IsZero() == %t, wants %t", c.CreatedAt.IsZero(), false) } } From 5ce17c88568720c110de3494427f14da17078b95 Mon Sep 17 00:00:00 2001 From: claudemiro Date: Sun, 6 Mar 2016 20:21:04 -0300 Subject: [PATCH 5/5] Code cleanup --- ipe/app.go | 6 +++--- ipe/app_test.go | 10 +++++----- ipe/channel_test.go | 8 ++++---- ipe/connection.go | 23 +++++++++++++++-------- ipe/connection_test.go | 8 ++------ ipe/context.go | 10 +++++----- ipe/handlers.go | 14 +++++++------- ipe/handlers_test.go | 4 ++-- ipe/ipe.go | 2 +- ipe/router.go | 12 +++++------- 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/ipe/app.go b/ipe/app.go index 0c77a47..ed7aec1 100644 --- a/ipe/app.go +++ b/ipe/app.go @@ -27,10 +27,10 @@ type app struct { WebHooks bool URLWebHook string - Channels map[string]*channel `json:"-"` - Connections map[string]*connection `json:"-"` + Channels map[string]*channel + Connections map[string]*connection - Stats *expvar.Map `json:"-"` + Stats *expvar.Map } func newApp(name, appID, key, secret string, onlySSL, disabled, userEvents, webHooks bool, webHookURL string) *app { diff --git a/ipe/app_test.go b/ipe/app_test.go index 2eeefcc..a6e07a8 100644 --- a/ipe/app_test.go +++ b/ipe/app_test.go @@ -22,7 +22,7 @@ func newTestApp() *app { func TestConnect(t *testing.T) { app := newTestApp() - app.Connect(newConnection("socketID", nil)) + app.Connect(newConnection("socketID", mockSocket{})) if len(app.Connections) != 1 { t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 1) @@ -33,7 +33,7 @@ func TestConnect(t *testing.T) { func TestDisconnect(t *testing.T) { app := newTestApp() - app.Connect(newConnection("socketID", nil)) + app.Connect(newConnection("socketID", mockSocket{})) app.Disconnect("socketID") if len(app.Connections) != 0 { @@ -45,7 +45,7 @@ func TestDisconnect(t *testing.T) { func TestFindConnection(t *testing.T) { app := newTestApp() - app.Connect(newConnection("socketID", nil)) + app.Connect(newConnection("socketID", mockSocket{})) if _, err := app.FindConnection("socketID"); err != nil { t.Errorf("app.FindConnection('socketID') == _, %q, wants %v", err, nil) @@ -165,7 +165,7 @@ func Test_New_Subscriber(t *testing.T) { t.Errorf("len(app.Connections) == %d, wants %d", len(app.Connections), 0) } - conn := newConnection("1", nil) + conn := newConnection("1", mockSocket{}) app.Connect(conn) if len(app.Connections) != 1 { @@ -175,7 +175,7 @@ func Test_New_Subscriber(t *testing.T) { func Test_find_subscriber(t *testing.T) { app := newTestApp() - conn := newConnection("1", nil) + conn := newConnection("1", mockSocket{}) app.Connect(conn) conn, err := app.FindConnection("1") diff --git a/ipe/channel_test.go b/ipe/channel_test.go index c42b09e..8eeb23c 100644 --- a/ipe/channel_test.go +++ b/ipe/channel_test.go @@ -13,7 +13,7 @@ func TestIsOccupied(t *testing.T) { t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), false) } - c.Subscriptions["ID"] = newSubscription(newConnection("ID", nil), "") + c.Subscriptions["ID"] = newSubscription(newConnection("ID", mockSocket{}), "") if !c.IsOccupied() { t.Errorf("c.IsOccupied() == %t, wants %t", c.IsOccupied(), true) @@ -69,8 +69,8 @@ func TestTotalSubscriptions(t *testing.T) { func TestTotalUsers(t *testing.T) { c := newChannel("ID") - c.Subscriptions["1"] = newSubscription(newConnection("ID", nil), "") - c.Subscriptions["2"] = newSubscription(newConnection("ID", nil), "") + c.Subscriptions["1"] = newSubscription(newConnection("ID", mockSocket{}), "") + c.Subscriptions["2"] = newSubscription(newConnection("ID", mockSocket{}), "") if c.TotalSubscriptions() != len(c.Subscriptions) { t.Errorf("c.TotalSubscriptions() == %d, wants %d", c.TotalSubscriptions(), len(c.Subscriptions)) @@ -84,7 +84,7 @@ func TestTotalUsers(t *testing.T) { func TestIsSubscribed(t *testing.T) { c := newChannel("ID") - conn := newConnection("ID", nil) + conn := newConnection("ID", mockSocket{}) if c.IsSubscribed(conn) { t.Errorf("c.IsSubscribed(%q) == %t, wants %t", conn, c.IsSubscribed(conn), false) diff --git a/ipe/connection.go b/ipe/connection.go index 14876a0..70419dc 100644 --- a/ipe/connection.go +++ b/ipe/connection.go @@ -8,18 +8,30 @@ import ( "time" log "github.com/golang/glog" - "github.com/gorilla/websocket" ) +// socket interface to write to the client +type socket interface { + WriteJSON(interface{}) error +} + +// mockSocket is a mock implementation of socket +// used in the test suite +type mockSocket struct{} + +func (s mockSocket) WriteJSON(i interface{}) error { + return nil +} + // An User Connection type connection struct { SocketID string - Socket *websocket.Conn + Socket socket CreatedAt time.Time } // Create a new Subscriber -func newConnection(socketID string, s *websocket.Conn) *connection { +func newConnection(socketID string, s socket) *connection { log.Infof("Creating a new Subscriber %+v", socketID) return &connection{SocketID: socketID, Socket: s, CreatedAt: time.Now()} @@ -28,11 +40,6 @@ func newConnection(socketID string, s *websocket.Conn) *connection { // Publish the message to websocket atached to this client func (conn *connection) Publish(m interface{}) { go func() { - if conn.Socket == nil { - log.Info("Socket is nil. Maybe you are testing this app?") - return - } - if err := conn.Socket.WriteJSON(m); err != nil { log.Errorf("Error publishing message to connection %+v, %s", conn, err) } diff --git a/ipe/connection_test.go b/ipe/connection_test.go index 34e7915..3d0f058 100644 --- a/ipe/connection_test.go +++ b/ipe/connection_test.go @@ -4,15 +4,11 @@ package ipe -import ( - "testing" - - "github.com/gorilla/websocket" -) +import "testing" func TestNewConnection(t *testing.T) { expectedSocketID := "socketID" - expectedSocket := &websocket.Conn{} + expectedSocket := mockSocket{} c := newConnection(expectedSocketID, expectedSocket) diff --git a/ipe/context.go b/ipe/context.go index bd98b0d..2caaa2a 100644 --- a/ipe/context.go +++ b/ipe/context.go @@ -17,13 +17,13 @@ func (p params) Get(key string) string { return p[key] } -// A handlerHTTPC responds to an HTTP request with custom application context. -type handlerHTTPC interface { - ServeHTTPC(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) +// A contextHandler responds to an HTTP request with custom application context. +type contextHandler interface { + ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) } -type handlerHTTPCFunc func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) +type contextHandlerFunc func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) -func (c handlerHTTPCFunc) ServeHTTPC(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { +func (c contextHandlerFunc) ServeWithContext(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { c(ctx, p, w, r) } diff --git a/ipe/handlers.go b/ipe/handlers.go index b279e95..6c2b1f5 100644 --- a/ipe/handlers.go +++ b/ipe/handlers.go @@ -46,8 +46,8 @@ func prepareQueryString(params url.Values) string { // * The request path (e.g. /some/resource) // * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string. // Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else) -func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerHTTPC { - return handlerHTTPCFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { +func restAuthenticationHandler(ctx *applicationContext, h contextHandler) contextHandler { + return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { appID := p.Get("app_id") app, err := ctx.DB.GetAppByAppID(appID) @@ -68,7 +68,7 @@ func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerH toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature { - h.ServeHTTPC(ctx, p, w, r) + h.ServeWithContext(ctx, p, w, r) } else { log.Error("Not authorized") http.Error(w, "Not authorized", http.StatusUnauthorized) @@ -77,8 +77,8 @@ func restAuthenticationHandler(ctx *applicationContext, h handlerHTTPC) handlerH } // Check if the application is disabled -func restCheckAppDisabledHandler(ctx *applicationContext, h handlerHTTPC) handlerHTTPC { - return handlerHTTPCFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { +func restCheckAppDisabledHandler(ctx *applicationContext, h contextHandler) contextHandler { + return contextHandlerFunc(func(ctx *applicationContext, p params, w http.ResponseWriter, r *http.Request) { appID := p.Get("app_id") currentApp, err := ctx.DB.GetAppByAppID(appID) @@ -93,12 +93,12 @@ func restCheckAppDisabledHandler(ctx *applicationContext, h handlerHTTPC) handle return } - h.ServeHTTPC(ctx, p, w, r) + h.ServeWithContext(ctx, p, w, r) }) } // commonHandlers combine restCheckAppDisabledHandler and restAuthenticationHandler handlers -func commonHandlers(ctx *applicationContext, h handlerHTTPCFunc) handlerHTTPC { +func commonHandlers(ctx *applicationContext, h contextHandlerFunc) contextHandler { return restCheckAppDisabledHandler(ctx, restAuthenticationHandler(ctx, h)) } diff --git a/ipe/handlers_test.go b/ipe/handlers_test.go index 8f3af12..d1cdc8e 100644 --- a/ipe/handlers_test.go +++ b/ipe/handlers_test.go @@ -21,10 +21,10 @@ func init() { testApp.AddChannel(newChannel("c2")) testApp.AddChannel(newChannel("private-c3")) - conn := newConnection("123.456", nil) + conn := newConnection("123.456", mockSocket{}) testApp.Subscribe(channel, conn, "{}") - conn = newConnection("321.654", nil) + conn = newConnection("321.654", mockSocket{}) testApp.Subscribe(channel, conn, "{}") db := newMemdb() diff --git a/ipe/ipe.go b/ipe/ipe.go index bd710d6..1811a40 100644 --- a/ipe/ipe.go +++ b/ipe/ipe.go @@ -53,7 +53,7 @@ func Start(filename string) { router.GET("/apps/{app_id}/channels/{channel_name}/users", commonHandlers(ctx, getChannelUsers)) - router.GET("/app/{key}", handlerHTTPCFunc(wsHandler)) + router.GET("/app/{key}", contextHandlerFunc(wsHandler)) if conf.SSL { go func() { diff --git a/ipe/router.go b/ipe/router.go index d39d4a3..21e53be 100644 --- a/ipe/router.go +++ b/ipe/router.go @@ -13,7 +13,7 @@ import ( type router struct { ctx *applicationContext mux *mux.Router - routes map[string]handlerHTTPC + routes map[string]contextHandler } func newRouter(ctx *applicationContext) *router { @@ -23,19 +23,17 @@ func newRouter(ctx *applicationContext) *router { } } -func (a *router) GET(path string, handler handlerHTTPC) { +func (a *router) GET(path string, handler contextHandler) { a.Handle("GET", path, handler) } -func (a *router) POST(path string, handler handlerHTTPC) { +func (a *router) POST(path string, handler contextHandler) { a.Handle("POST", path, handler) } -func (a *router) Handle(method, path string, handler handlerHTTPC) { +func (a *router) Handle(method, path string, handler contextHandler) { a.mux.Methods(method).Path(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p := params(mux.Vars(r)) - - handler.ServeHTTPC(a.ctx, p, w, r) + handler.ServeWithContext(a.ctx, params(mux.Vars(r)), w, r) }) }