Skip to content

Commit

Permalink
feat: add session adapter to mount gorilla store
Browse files Browse the repository at this point in the history
  • Loading branch information
shaj13 committed May 22, 2020
1 parent ada6dbe commit 76ff76d
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 0 deletions.
35 changes: 35 additions & 0 deletions store/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package store

import (
"fmt"
"net/http"
"net/http/httptest"

"github.com/gorilla/sessions"
)

func ExampleSession() {
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
store := sessions.NewCookieStore([]byte("key"))
sessionCache := &Session{
Name: "seesion-key",
GStore: store,
}

sessionCache.Store("key", "value", r)
SetCookie(w, r)

v, ok, _ := sessionCache.Load("key", r)
fmt.Println(v, ok)

sessionCache.Delete("key", r)
SetCookie(w, r)

v, ok, _ = sessionCache.Load("key", r)
fmt.Println(v, ok)

// Output:
// value true
// <nil> false
}
104 changes: 104 additions & 0 deletions store/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package store

import (
"context"
"net/http"

"github.com/gorilla/sessions"
)

// Session implements Cache and provide adaptation to gorilla session store to work with the auth packages.
type Session struct {
// Name represents session name will be passed to gorilla store
Name string
// GStore represents the gorilla session store.
GStore sessions.Store
}

// Load returns the value stored in the Session for a key, or nil if no value is present.
// The ok result indicates whether value was found in the Session.
// The error returned if an error occurs, Otherwise nil.
func (s *Session) Load(key string, r *http.Request) (interface{}, bool, error) {
session, err := s.GStore.Get(r, s.Name)
if err != nil {
return nil, false, err
}
v, ok := session.Values[key]
return v, ok, nil
}

// Store sets the value for a key.
// The error returned if an error occurs, Otherwise nil.
func (s *Session) Store(key string, value interface{}, r *http.Request) error {
session, err := s.GStore.Get(r, s.Name)
if err != nil {
return err
}

session.Values[key] = value
writer := &cookieRecorder{
header: make(http.Header),
}

err = s.GStore.Save(r, writer, session)
if err != nil {
return err
}

// r.WithContext() return a shallow copy of r.
// we must change underlying request so the context value takes effect
*r = *requestWithCookie(r, writer.Header())
return nil
}

// Delete the value for a key.
// The error returned if an error occurs, Otherwise nil
func (s *Session) Delete(key string, r *http.Request) error {
session, err := s.GStore.Get(r, s.Name)
if err != nil {
return err
}

delete(session.Values, key)
session.Options.MaxAge = -1
writer := &cookieRecorder{
header: make(http.Header),
}

err = s.GStore.Save(r, writer, session)
if err != nil {
return err
}

// r.WithContext() return a shallow copy of r.
// we must change underlying request so the context value takes effect
*r = *requestWithCookie(r, writer.Header())
return nil
}

type cookieRecorder struct {
header http.Header
}

func (c *cookieRecorder) Header() http.Header { return c.header }
func (c *cookieRecorder) Write([]byte) (int, error) { return 0, nil }
func (c *cookieRecorder) WriteHeader(int) {}

type cookieKey struct{}

func requestWithCookie(r *http.Request, h http.Header) *http.Request {
cookie := h.Get("Set-Cookie")
ctx := context.WithValue(r.Context(), cookieKey{}, cookie)
return r.WithContext(ctx)
}

// SetCookie adds a Set-Cookie header to the provided ResponseWriter's header.
// Use this function only when the cache instance of type session.
// Should be invoked before return response back to the end-users.
func SetCookie(w http.ResponseWriter, r *http.Request) {
if v := r.Context().Value(cookieKey{}); v != nil {
if cookie, ok := v.(string); ok {
w.Header().Set("Set-Cookie", cookie)
}
}
}
211 changes: 211 additions & 0 deletions store/session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package store

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/sessions"
"github.com/stretchr/testify/assert"
)

func TestSession(t *testing.T) {
table := []struct {
name string
key string
value interface{}
op string
getErr bool
saveErr bool
found bool
}{
{
name: "it return error on Get session when trying to load a value",
op: "load",
getErr: true,
},
{
name: "it return error Get session when trying to store a value",
op: "store",
getErr: true,
},
{
name: "it return error Get session when trying to delete a value",
op: "store",
getErr: true,
},
{
name: "it return error Save session when trying to store a value",
op: "store",
saveErr: true,
},
{
name: "it return error Save session when trying to delete a value",
op: "store",
saveErr: true,
},
{
name: "it return false when key does not exist",
op: "load",
key: "key",
found: false,
},
{
name: "it return true and value when exist",
op: "load",
// key/value its loaded as default in mock store See L131
key: "test",
value: "test",
found: true,
},
{
name: "it overwrite exist key and value when store",
op: "store",
// key/value its loaded as default in mock store See L131
key: "test",
value: "test2",
found: true,
},
{
name: "it create new record when store",
op: "store",
// key/value its loaded as default in mock store See L131
key: "key",
value: "value",
found: true,
},
{
name: "it's not crash when trying to delete a non exist record",
op: "delete",
// key/value its loaded as default in mock store See L131
key: "key",
found: false,
},
{
name: "it delete a exist record",
op: "delete",
// key/value its loaded as default in mock store See L131
key: "test",
found: false,
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
store := &mockGStore{
getErr: tt.getErr,
saveErr: tt.saveErr,
}

sessionCache := &Session{
GStore: store,
Name: "test",
}

r, _ := http.NewRequest("GET", "/", nil)
store.New(r, "")
var err error

switch tt.op {
case "load":
var v interface{}
var ok bool
v, ok, err = sessionCache.Load(tt.key, r)
assert.Equal(t, tt.value, v)
assert.Equal(t, tt.found, ok)
case "store":
err = sessionCache.Store(tt.key, tt.value, r)
case "delete":
err = sessionCache.Delete(tt.key, r)
}

if tt.getErr || tt.saveErr {
assert.True(t, err != nil)
return
}

assert.NoError(t, err)

// after each op load and assert
v, ok := store.session.Values[tt.key]
assert.Equal(t, tt.value, v)
assert.Equal(t, tt.found, ok)

// assert cookie eist on request context
if tt.op != "load" {
assert.Equal(t, "mockGStore-Cookie", r.Context().Value(cookieKey{}))
}

})
}
}

func TestSetCookie(t *testing.T) {
table := []struct {
name string
key interface{}
value interface{}
expected string
}{
{
name: "it not set cookie when cookiekey does not exist in context",
key: "sample",
},
{
name: "it not set cookie when cookiekey value not of type string",
key: cookieKey{},
value: 1,
},
{
name: "it set cookie when pass all validation",
key: cookieKey{},
value: "cookie",
expected: "cookie",
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
ctx := context.WithValue(r.Context(), tt.key, tt.value)
r = r.WithContext(ctx)
SetCookie(w, r)
assert.Equal(t, tt.expected, w.Header().Get("Set-Cookie"))
})
}
}

type mockGStore struct {
getErr bool
saveErr bool
session *sessions.Session
}

func (m *mockGStore) Get(r *http.Request, name string) (*sessions.Session, error) {
if m.getErr {
return nil, fmt.Errorf("mock GStore error, L32")
}
return m.New(r, name)
}

func (m *mockGStore) Save(_ *http.Request, w http.ResponseWriter, s *sessions.Session) error {
if m.saveErr {
return fmt.Errorf("mock GStore error, L32")
}
w.Header().Set("Set-Cookie", "mockGStore-Cookie")
m.session = s
return nil
}

func (m *mockGStore) New(_ *http.Request, _ string) (*sessions.Session, error) {
m.session = &sessions.Session{
Values: map[interface{}]interface{}{
"test": "test",
},
Options: &sessions.Options{},
}
return m.session, nil
}

0 comments on commit 76ff76d

Please sign in to comment.