From 2ce70bcb0eaae1c5ac09f773fa89332b47d88bd5 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Mon, 3 Jun 2024 15:22:45 -0300 Subject: [PATCH] feat: add mockstorage --- .github/workflows/test-mockstorage.yml | 28 +++ mockstorage/README.md | 131 ++++++++++++ mockstorage/go.mod | 3 + mockstorage/go.sum | 0 mockstorage/mockstorage.go | 170 +++++++++++++++ mockstorage/mockstorage_test.go | 276 +++++++++++++++++++++++++ 6 files changed, 608 insertions(+) create mode 100644 .github/workflows/test-mockstorage.yml create mode 100644 mockstorage/README.md create mode 100644 mockstorage/go.mod create mode 100644 mockstorage/go.sum create mode 100644 mockstorage/mockstorage.go create mode 100644 mockstorage/mockstorage_test.go diff --git a/.github/workflows/test-mockstorage.yml b/.github/workflows/test-mockstorage.yml new file mode 100644 index 00000000..befbaa0d --- /dev/null +++ b/.github/workflows/test-mockstorage.yml @@ -0,0 +1,28 @@ +on: + push: + branches: + - master + - main + paths: + - 'mockstorage/**' + pull_request: + paths: + - 'mockstorage/**' +name: "Tests Local Storage" +jobs: + Tests: + strategy: + matrix: + go-version: + - 1.21.x + - 1.22.x + runs-on: ubuntu-latest + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '${{ matrix.go-version }}' + - name: Test Mockstorage + run: cd ./mockstorage && go test ./... -v -race diff --git a/mockstorage/README.md b/mockstorage/README.md new file mode 100644 index 00000000..290eac55 --- /dev/null +++ b/mockstorage/README.md @@ -0,0 +1,131 @@ +--- +id: memory +title: Memory +--- + + +![Release](https://img.shields.io/github/v/tag/gofiber/storage?filter=mockstorage*) +[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) +![Test](https://img.shields.io/github/actions/workflow/status/gofiber/storage/test-mockstorage.yml?label=Tests) +![Security](https://img.shields.io/github/actions/workflow/status/gofiber/storage/gosec.yml?label=Security) +![Linter](https://img.shields.io/github/actions/workflow/status/gofiber/storage/linter.yml?label=Linter) + +A mock storage implementation for Fiber. This storage is not persistent and is only used for testing purposes. + +**Note: Requires Go 1.21 and above** + +### Table of Contents +- [Signatures](#signatures) +- [Installation](#installation) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config ...Config) *Storage +func (s *Storage) Get(key string) ([]byte, error) +func (s *Storage) Set(key string, val []byte, exp time.Duration) error +func (s *Storage) Delete(key string) error +func (s *Storage) Reset() error +func (s *Storage) Close() error +func (s *Storage) Conn() map[string]entry +func (s *Storage) Keys() ([][]byte, error) + +func SetCustomFuncs(custom *CustomFuncs) +``` + +### Installation +Mockstorage is tested on the 2 last [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet: +```bash +go mod init github.com// +``` +And then install the mockstorage implementation: +```bash +go get github.com/gofiber/storage/mockstorage +``` + +### Examples +Import the storage package. +```go +import "github.com/gofiber/storage/mockstorage" +``` + +You can use the following possibilities to create a storage: +```go +// Initialize default config +store := mockstorage.New() + +// Initialize custom config +store := mockstorage.New(mockstorage.Config{ + CustomFuncs: &mockstorage.CustomFuncs{ + &CustomFuncs{ + GetFunc: func(key string) ([]byte, error) { + if key == "customKey" { + return []byte("customValue"), nil + } + return nil, errors.New("custom key not found") + }, + SetFunc: func(key string, val []byte, exp time.Duration) error { + if key == "readonly" { + return errors.New("cannot set readonly key") + } + return nil + }, + DeleteFunc: func(key string) error { + if key == "protectedKey" { + return errors.New("cannot delete protected key") + } + return nil + }, + // ... + }, + }, +}) + +// Set custom functions after initialization +store.SetCustomFuncs(&mockstorage.CustomFuncs{ + GetFunc: func(key string) ([]byte, error) { + if key == "customKey" { + return []byte("customValue"), nil + } + return nil, errors.New("custom key not found") + }, + SetFunc: func(key string, val []byte, exp time.Duration) error { + if key == "readonly" { + return errors.New("cannot set readonly key") + } + return nil + }, + DeleteFunc: func(key string) error { + if key == "protectedKey" { + return errors.New("cannot delete protected key") + } + return nil + }, + // ... +}) +``` + +### Config +```go +type Config struct { + CustomFuncs *CustomFuncs +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + CustomFuncs: &CustomFuncs{ + GetFunc: nil, + SetFunc: nil, + DeleteFunc: nil, + ResetFunc: nil, + CloseFunc: nil, + ConnFunc: nil, + KeysFunc: nil, + }, +} +``` diff --git a/mockstorage/go.mod b/mockstorage/go.mod new file mode 100644 index 00000000..812c0c9f --- /dev/null +++ b/mockstorage/go.mod @@ -0,0 +1,3 @@ +module mockstorage + +go 1.21 diff --git a/mockstorage/go.sum b/mockstorage/go.sum new file mode 100644 index 00000000..e69de29b diff --git a/mockstorage/mockstorage.go b/mockstorage/mockstorage.go new file mode 100644 index 00000000..9575a133 --- /dev/null +++ b/mockstorage/mockstorage.go @@ -0,0 +1,170 @@ +package mockstorage + +import ( + "errors" + "sync" + "time" +) + +// Config defines the config for mock storage. +type Config struct { + CustomFuncs *CustomFuncs +} + +// Storage is the mock storage adapter. +type Storage struct { + mu sync.RWMutex + data map[string]entry + custom *CustomFuncs +} + +// entry struct to hold value and expiration time. +type entry struct { + value []byte + exp time.Time +} + +// CustomFuncs allows injecting custom behaviors for testing. +type CustomFuncs struct { + GetFunc func(key string) ([]byte, error) + SetFunc func(key string, val []byte, exp time.Duration) error + DeleteFunc func(key string) error + ResetFunc func() error + CloseFunc func() error + ConnFunc func() map[string]entry + KeysFunc func() ([][]byte, error) +} + +// New creates a new mock storage with optional configuration. +func New(config ...Config) *Storage { + s := &Storage{ + data: make(map[string]entry), + custom: &CustomFuncs{ + GetFunc: nil, + SetFunc: nil, + DeleteFunc: nil, + ResetFunc: nil, + CloseFunc: nil, + ConnFunc: nil, + KeysFunc: nil, + }, + } + + // If a config is provided and it has CustomFuncs, use them + if len(config) > 0 && config[0].CustomFuncs != nil { + s.custom = config[0].CustomFuncs + } + + return s +} + +// Get retrieves the value for a given key. +func (s *Storage) Get(key string) ([]byte, error) { + if s.custom.GetFunc != nil { + return s.custom.GetFunc(key) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + e, ok := s.data[key] + if !ok { + return nil, errors.New("key not found") + } + if !e.exp.IsZero() && time.Now().After(e.exp) { + delete(s.data, key) + return nil, errors.New("key expired") + } + return e.value, nil +} + +// Set sets the value for a given key with an expiration time. +func (s *Storage) Set(key string, val []byte, exp time.Duration) error { + if s.custom.SetFunc != nil { + return s.custom.SetFunc(key, val, exp) + } + + s.mu.Lock() + defer s.mu.Unlock() + + var expTime time.Time + if exp > 0 { + expTime = time.Now().Add(exp) + } + + s.data[key] = entry{value: val, exp: expTime} + return nil +} + +// Delete removes a key from the storage. +func (s *Storage) Delete(key string) error { + if s.custom.DeleteFunc != nil { + return s.custom.DeleteFunc(key) + } + + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.data, key) + return nil +} + +// Reset clears all keys from the storage. +func (s *Storage) Reset() error { + if s.custom.ResetFunc != nil { + return s.custom.ResetFunc() + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.data = make(map[string]entry) + return nil +} + +// Close closes the storage (no-op for mock). +func (s *Storage) Close() error { + if s.custom.CloseFunc != nil { + return s.custom.CloseFunc() + } + + // No resources to clean up in mock + return nil +} + +// Conn returns the internal data map (for testing purposes). +func (s *Storage) Conn() map[string]entry { + if s.custom.ConnFunc != nil { + return s.custom.ConnFunc() + } + + s.mu.RLock() + defer s.mu.RUnlock() + + copyData := make(map[string]entry) + for k, v := range s.data { + copyData[k] = v + } + return copyData +} + +// Keys returns all keys in the storage. +func (s *Storage) Keys() ([][]byte, error) { + if s.custom.KeysFunc != nil { + return s.custom.KeysFunc() + } + + s.mu.RLock() + defer s.mu.RUnlock() + + keys := make([][]byte, 0, len(s.data)) + for k := range s.data { + keys = append(keys, []byte(k)) + } + return keys, nil +} + +// SetCustomFuncs allows setting custom function implementations. +func (s *Storage) SetCustomFuncs(custom *CustomFuncs) { + s.custom = custom +} diff --git a/mockstorage/mockstorage_test.go b/mockstorage/mockstorage_test.go new file mode 100644 index 00000000..8777cbe1 --- /dev/null +++ b/mockstorage/mockstorage_test.go @@ -0,0 +1,276 @@ +package mockstorage + +import ( + "bytes" + "errors" + "testing" + "time" +) + +func TestStorageDefaultBehavior(t *testing.T) { + store := New() + + // Test Set and Get + err := store.Set("key1", []byte("value1"), 0) + if err != nil { + t.Fatalf("Set() error = %v, wantErr %v", err, nil) + } + + val, err := store.Get("key1") + if err != nil { + t.Fatalf("Get() error = %v, wantErr %v", err, nil) + } + if !bytes.Equal(val, []byte("value1")) { + t.Errorf("Get() = %v, want %v", val, []byte("value1")) + } + + // Test Delete + err = store.Delete("key1") + if err != nil { + t.Fatalf("Delete() error = %v, wantErr %v", err, nil) + } + + _, err = store.Get("key1") + if err == nil { + t.Errorf("Get() error = %v, wantErr %v", err, "key not found") + } + + // Test Reset + err = store.Set("key2", []byte("value2"), 0) + if err != nil { + t.Fatalf("Set() error = %v, wantErr %v", err, nil) + } + + err = store.Reset() + if err != nil { + t.Fatalf("Reset() error = %v, wantErr %v", err, nil) + } + + _, err = store.Get("key2") + if err == nil { + t.Errorf("Get() error = %v, wantErr %v", err, "key not found") + } + + // Test Expiry + err = store.Set("key3", []byte("value3"), time.Millisecond*100) + if err != nil { + t.Fatalf("Set() error = %v, wantErr %v", err, nil) + } + time.Sleep(time.Millisecond * 200) + + _, err = store.Get("key3") + if err == nil { + t.Errorf("Get() error = %v, wantErr %v", err, "key expired") + } +} + +func TestStorageConnFunc(t *testing.T) { + store := New() + + customFuncs := &CustomFuncs{ + ConnFunc: func() map[string]entry { + return map[string]entry{ + "customKey1": {value: []byte("customValue1"), exp: time.Time{}}, + "customKey2": {value: []byte("customValue2"), exp: time.Now().Add(1 * time.Hour)}, + } + }, + } + + store.SetCustomFuncs(customFuncs) + + // Test custom Conn + conn := store.Conn() + expectedConn := map[string]entry{ + "customKey1": {value: []byte("customValue1"), exp: time.Time{}}, + "customKey2": {value: []byte("customValue2"), exp: time.Now().Add(1 * time.Hour)}, + } + + for k, v := range expectedConn { + if val, ok := conn[k]; !ok || !bytes.Equal(val.value, v.value) { + t.Errorf("Conn() = %v, want %v", conn, expectedConn) + } + } +} + +func TestResetFunc(t *testing.T) { + store := New() + + customFuncs := &CustomFuncs{ + ResetFunc: func() error { + return errors.New("reset error") + }, + } + + store.SetCustomFuncs(customFuncs) + + err := store.Reset() + if err == nil { + t.Errorf("Reset() error = %v, wantErr %v", err, "reset error") + } +} + +func TestStorageCloseFunc(t *testing.T) { + store := New() + + customFuncs := &CustomFuncs{ + CloseFunc: func() error { + return errors.New("close error") + }, + } + + store.SetCustomFuncs(customFuncs) + + err := store.Close() + if err == nil { + t.Errorf("Close() error = %v, wantErr %v", err, "close error") + } +} + +func TestStorageKeysFunc(t *testing.T) { + store := New() + + customFuncs := &CustomFuncs{ + KeysFunc: func() ([][]byte, error) { + return [][]byte{[]byte("customKey1"), []byte("customKey2")}, nil + }, + } + + store.SetCustomFuncs(customFuncs) + + // Test custom Keys + keys, err := store.Keys() + if err != nil { + t.Fatalf("Keys() error = %v, wantErr %v", err, nil) + } + expectedKeys := [][]byte{[]byte("customKey1"), []byte("customKey2")} + if len(keys) != len(expectedKeys) { + t.Fatalf("Keys() = %v, want %v", keys, expectedKeys) + } + for i, key := range expectedKeys { + if !bytes.Equal(keys[i], key) { + t.Errorf("Keys() = %v, want %v", keys, expectedKeys) + } + } +} + +func TestStorageCustomBehavior(t *testing.T) { + store := New() + customFuncs := &CustomFuncs{ + GetFunc: func(key string) ([]byte, error) { + if key == "customKey" { + return []byte("customValue"), nil + } + return nil, errors.New("custom key not found") + }, + SetFunc: func(key string, val []byte, exp time.Duration) error { + if key == "readonly" { + return errors.New("cannot set readonly key") + } + return nil + }, + DeleteFunc: func(key string) error { + if key == "protectedKey" { + return errors.New("cannot delete protected key") + } + return nil + }, + ConnFunc: func() map[string]entry { + return map[string]entry{ + "customKey1": {value: []byte("customValue1"), exp: time.Time{}}, + "customKey2": {value: []byte("customValue2"), exp: time.Now().Add(1 * time.Hour)}, + } + }, + KeysFunc: func() ([][]byte, error) { + return [][]byte{[]byte("customKey1"), []byte("customKey2")}, nil + }, + } + + store.SetCustomFuncs(customFuncs) + + // Test custom Get + val, err := store.Get("customKey") + if err != nil { + t.Fatalf("Get() error = %v, wantErr %v", err, nil) + } + if !bytes.Equal(val, []byte("customValue")) { + t.Errorf("Get() = %v, want %v", val, []byte("customValue")) + } + + _, err = store.Get("unknownKey") + if err == nil { + t.Errorf("Get() error = %v, wantErr %v", err, "custom key not found") + } + + // Test custom Set + err = store.Set("readonly", []byte("value"), 0) + if err == nil { + t.Errorf("Set() error = %v, wantErr %v", err, "cannot set readonly key") + } + + err = store.Set("regularKey", []byte("value"), 0) + if err != nil { + t.Fatalf("Set() error = %v, wantErr %v", err, nil) + } + + // Test custom Delete + err = store.Delete("protectedKey") + if err == nil { + t.Errorf("Delete() error = %v, wantErr %v", err, "cannot delete protected key") + } + + err = store.Delete("regularKey") + if err != nil { + t.Fatalf("Delete() error = %v, wantErr %v", err, nil) + } + + // Test custom Conn + conn := store.Conn() + expectedConn := map[string]entry{ + "customKey1": {value: []byte("customValue1"), exp: time.Time{}}, + "customKey2": {value: []byte("customValue2"), exp: time.Now().Add(1 * time.Hour)}, + } + + for k, v := range expectedConn { + if val, ok := conn[k]; !ok || !bytes.Equal(val.value, v.value) { + t.Errorf("Conn() = %v, want %v", conn, expectedConn) + } + } + + // Test custom Keys + keys, err := store.Keys() + if err != nil { + t.Fatalf("Keys() error = %v, wantErr %v", err, nil) + } + expectedKeys := [][]byte{[]byte("customKey1"), []byte("customKey2")} + if len(keys) != len(expectedKeys) { + t.Fatalf("Keys() = %v, want %v", keys, expectedKeys) + } + for i, key := range expectedKeys { + if !bytes.Equal(keys[i], key) { + t.Errorf("Keys() = %v, want %v", keys, expectedKeys) + } + } +} + +func TestStorageConnAndKeys(t *testing.T) { + store := New() + + // Test Conn + err := store.Set("key1", []byte("value1"), 0) + if err != nil { + t.Fatalf("Set() error = %v, wantErr %v", err, nil) + } + conn := store.Conn() + if val, ok := conn["key1"]; !ok || !bytes.Equal(val.value, []byte("value1")) { + t.Errorf("Conn() = %v, want %v", conn, map[string]entry{"key1": {value: []byte("value1"), exp: time.Time{}}}) + } + + // Test Keys + keys, err := store.Keys() + if err != nil { + t.Fatalf("Keys() error = %v, wantErr %v", err, nil) + } + if len(keys) != 1 || !bytes.Equal(keys[0], []byte("key1")) { + t.Errorf("Keys() = %v, want %v", keys, [][]byte{[]byte("key1")}) + } +}