-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add session adapter to mount gorilla store
- Loading branch information
Showing
3 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |