Skip to content
This repository has been archived by the owner on Sep 21, 2022. It is now read-only.

Commit

Permalink
Add session and flash message support
Browse files Browse the repository at this point in the history
This add support on the *baseContext for sessions using gorilla/sessions
package.

Additionally a basic flash messages implementation is added which uses
the session implementation.
  • Loading branch information
gernest committed Dec 17, 2016
1 parent 1041f55 commit cd0917a
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 5 deletions.
6 changes: 6 additions & 0 deletions base/context.go
@@ -1,3 +1,6 @@
// Package base is the basic building cblock of utron. The main structure here is
// Context, but for some reasons to avoid confusion since there is a lot of
// context packages I decided to name this package base instead.
package base

import (
Expand All @@ -12,6 +15,7 @@ import (
"github.com/gernest/utron/view"
"github.com/gorilla/context"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
)

// Content holds http response content type strings
Expand Down Expand Up @@ -59,6 +63,8 @@ type Context struct {

Log logger.Logger

SessionStore sessions.Store

request *http.Request
response http.ResponseWriter
out io.ReadWriter
Expand Down
86 changes: 86 additions & 0 deletions base/context_test.go
@@ -0,0 +1,86 @@
package base

import (
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/gernest/utron/config"
"github.com/gorilla/mux"
)

type DummyView struct {
}

func (d *DummyView) Render(out io.Writer, name string, data interface{}) error {
out.Write([]byte(name))
return nil
}

func TestContext(t *testing.T) {
r := mux.NewRouter()
name := "world"
r.HandleFunc("/hello/{name}", testHandler(t, name))
req, _ := http.NewRequest("GET", "/hello/"+name, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}

func testHandler(t *testing.T, name string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctxHandler(t, name, w, r)
}
}

func ctxHandler(t *testing.T, name string, w http.ResponseWriter, r *http.Request) {
ctx := NewContext(w, r)
ctx.Init()
pname := ctx.Params["name"]
if pname != name {
t.Error("expected %s got %s", name, pname)
}

ctx.SetData("name", pname)

data := ctx.GetData("name")
if data == nil {
t.Error("expected values to be stored in context")
}
ctx.JSON()
h := w.Header().Get(Content.Type)
if h != Content.Application.JSON {
t.Errorf("expected %s got %s", Content.Application.JSON, h)
}
ctx.HTML()
h = w.Header().Get(Content.Type)
if h != Content.TextHTML {
t.Errorf("expected %s got %s", Content.TextHTML, h)
}
ctx.TextPlain()
h = w.Header().Get(Content.Type)
if h != Content.TextPlain {
t.Errorf("expected %s got %s", Content.TextPlain, h)
}

err := ctx.Commit()
if err != nil {
t.Error(err)
}

// make sure we can't commit twice
err = ctx.Commit()
if err == nil {
t.Error("expected error")
}

// when there is template and view
ctx.isCommited = false
ctx.Template = pname
ctx.Set(&DummyView{})
ctx.Cfg = &config.Config{}
err = ctx.Commit()
if err != nil {
t.Error(err)
}
}
36 changes: 36 additions & 0 deletions base/session.go
@@ -0,0 +1,36 @@
package base

import (
"errors"

"github.com/gorilla/sessions"
)

var errNoStore = errors.New("no session store was found")

//NewSession returns a new browser session whose key is set to name. This only
//works when the *Context.SessionStore is not nil.
//
// The session returned is from grorilla/sessions package.
func (ctx *Context) NewSession(name string) (*sessions.Session, error) {
if ctx.SessionStore != nil {
return ctx.SessionStore.New(ctx.Request(), name)
}
return nil, errNoStore
}

//GetSession retrieves session with a given name.
func (ctx *Context) GetSession(name string) (*sessions.Session, error) {
if ctx.SessionStore != nil {
return ctx.SessionStore.New(ctx.Request(), name)
}
return nil, errNoStore
}

//SaveSession saves the given session.
func (ctx *Context) SaveSession(s *sessions.Session) error {
if ctx.SessionStore != nil {
return ctx.SessionStore.Save(ctx.Request(), ctx.Response(), s)
}
return errNoStore
}
21 changes: 21 additions & 0 deletions base/session_test.go
@@ -0,0 +1,21 @@
package base

import "testing"

func TestContextSession(t *testing.T) {
// should error when the session store is not set
name := "sess"
ctx := &Context{}
_, err := ctx.NewSession(name)
if err == nil {
t.Error("expected error ", errNoStore)
}
_, err = ctx.GetSession(name)
if err == nil {
t.Error("expected error ", errNoStore)
}
err = ctx.SaveSession(nil)
if err == nil {
t.Error("expected error ", errNoStore)
}
}
3 changes: 2 additions & 1 deletion controller/controller.go
Expand Up @@ -16,7 +16,8 @@ type Controller interface {
// BaseController implements the Controller interface, It is recommended all
// user defined Controllers should embed *BaseController.
type BaseController struct {
Ctx *base.Context
Ctx *base.Context
Routes []string
}

// New sets ctx as the active context
Expand Down
120 changes: 120 additions & 0 deletions flash/flash.go
@@ -0,0 +1,120 @@
package base

import (
"encoding/gob"
"errors"

"github.com/gernest/utron/base"
)

const (
// FlashSuccess is the context key for success flash messages
FlashSuccess = "FlashSuccess"

// FlashWarn is a context key for warning flash messages
FlashWarn = "FlashWarn"

// FlashErr is a context key for flash error message
FlashErr = "FlashError"
)

func init() {
gob.Register(&Flash{})
gob.Register(Flashes{})
}

// Flash implements flash messages, like ones in gorilla/sessions
type Flash struct {
Kind string
Message string
}

// Flashes is a collection of flash messages
type Flashes []*Flash

// GetFlashes retieves all flash messages found in a cookie session associated with ctx..
//
// name is the session name which is used to store the flash messages. The flash
// messages can be stored in any session, but it is a good idea to separate
// session for flash messages from other sessions.
//
// key is the key that is used to identiry which flash messages are of interest.
func GetFlashes(ctx *base.Context, name, key string) (Flashes, error) {
ss, err := ctx.GetSession(name)
if err != nil {
return nil, err
}
if v, ok := ss.Values[key]; ok {
delete(ss.Values, key)
serr := ss.Save(ctx.Request(), ctx.Response())
if serr != nil {
return nil, serr
}
return v.(Flashes), nil
}
return nil, errors.New("no flashes found")
}

// AddFlashToCtx takes flash messages stored in a cookie which is associated with the
// request found in ctx, and puts them inside the ctx object. The flash messages can then
// be retrived by calling ctx.Get( FlashKey).
//
// NOTE When there are no flash messages then nothing is set.
func AddFlashToCtx(ctx *base.Context, name, key string) error {
f, err := GetFlashes(ctx, name, key)
if err != nil {
return err
}
ctx.SetData(key, f)
return nil
}

//Flasher tracks flash messages
type Flasher struct {
f Flashes
}

//New creates new flasher. This alllows accumulation of lash messages. To save the flash messages
//the Save method should be called explicitly.
func New() *Flasher {
return &Flasher{}
}

// Add adds the flash message
func (f *Flasher) Add(kind, message string) {
fl := &Flash{kind, message}
f.f = append(f.f, fl)
}

// Success adds success flash message
func (f *Flasher) Success(msg string) {
f.Add(FlashSuccess, msg)
}

// Err adds error flash message
func (f *Flasher) Err(msg string) {
f.Add(FlashErr, msg)
}

// Warn adds warning flash message
func (f *Flasher) Warn(msg string) {
f.Add(FlashWarn, msg)
}

// Save saves flash messages to context
func (f *Flasher) Save(ctx *base.Context, name, key string) error {
ss, err := ctx.GetSession(name)
if err != nil {
return err
}
var flashes Flashes
if v, ok := ss.Values[key]; ok {
flashes = v.(Flashes)
}
ss.Values[key] = append(flashes, f.f...)
err = ss.Save(ctx.Request(), ctx.Response())
if err != nil {
return err
}
return nil
}
83 changes: 83 additions & 0 deletions flash/flash_test.go
@@ -0,0 +1,83 @@
package base

import (
"fmt"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"os"
"testing"

"github.com/gernest/utron/controller"
"github.com/gernest/utron/logger"
"github.com/gernest/utron/router"
"github.com/gorilla/sessions"
)

type FlashTest struct {
controller.BaseController
Routes []string
}

const (
fname = "flash"
fkey = "flash"
)

var result Flashes

func (f *FlashTest) Index() {
fl := New()
fl.Success("Success")
fl.Err("Err")
fl.Warn("Warn")
fl.Save(f.Ctx, fname, fkey)
}

func (f FlashTest) Flash() {
r, err := GetFlashes(f.Ctx, fname, fkey)
if err != nil {
f.Ctx.Log.Errors(err)
return
}
result = r
}

func NewFlashTest() controller.Controller {
return &FlashTest{
Routes: []string{
"get;/;Index",
"get;/flash;Flash",
},
}
}

func TestFlash(t *testing.T) {
codecKey1 := "ePAPW9vJv7gHoftvQTyNj5VkWB52mlza"
codecKey2 := "N8SmpJ00aSpepNrKoyYxmAJhwVuKEWZD"
jar, err := cookiejar.New(nil)
if err != nil {
t.Error(err)
}
client := &http.Client{Jar: jar}
o := &router.Options{
Log: logger.NewDefaultLogger(os.Stdout),
SessionStore: sessions.NewCookieStore([]byte(codecKey1), []byte(codecKey2)),
}
r := router.NewRouter(o)
r.Add(NewFlashTest)
ts := httptest.NewServer(r)
defer ts.Close()
_, err = client.Get(fmt.Sprintf("%s/", ts.URL))
if err != nil {
t.Error(err)
}
_, err = client.Get(fmt.Sprintf("%s/flash", ts.URL))
if err != nil {
t.Error(err)
}

if len(result) != 3 {
t.Errorf("expected 3 got %d", len(result))
}
}

0 comments on commit cd0917a

Please sign in to comment.