From 2ce70bcb0eaae1c5ac09f773fa89332b47d88bd5 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Mon, 3 Jun 2024 15:22:45 -0300 Subject: [PATCH 01/10] 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")}) + } +} From 024059f926ab5f558c04b58df2b0b76f3fc8d887 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Mon, 3 Jun 2024 15:31:35 -0300 Subject: [PATCH 02/10] chore: mockstorage id/title --- mockstorage/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mockstorage/README.md b/mockstorage/README.md index 290eac55..28cd2216 100644 --- a/mockstorage/README.md +++ b/mockstorage/README.md @@ -1,6 +1,6 @@ --- -id: memory -title: Memory +id: mockstorage +title: MockStorage --- @@ -37,7 +37,7 @@ 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: +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// ``` From 800262d044b23c17eba73a1ccc3e5076a382db25 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 10:51:34 -0300 Subject: [PATCH 03/10] chore: update dependabot configuration for gomod package manifests in /mockstorage/ --- .github/dependabot.yml | 6 +++ .github/release-drafter-mockstorage.yml | 50 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/release-drafter-mockstorage.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4d5b2352..9b055340 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -80,6 +80,12 @@ updates: - "๐Ÿค– Dependencies" schedule: interval: "daily" + - package-ecosystem: "gomod" + directory: "/mockstorage/" # Location of package manifests + labels: + - "๐Ÿค– Dependencies" + schedule: + interval: "daily" - package-ecosystem: "gomod" directory: "/mongodb/" # Location of package manifests labels: diff --git a/.github/release-drafter-mockstorage.yml b/.github/release-drafter-mockstorage.yml new file mode 100644 index 00000000..62330f1b --- /dev/null +++ b/.github/release-drafter-mockstorage.yml @@ -0,0 +1,50 @@ +name-template: 'MockStorage - v$RESOLVED_VERSION' +tag-template: 'mockstorage/v$RESOLVED_VERSION' +tag-prefix: mockstorage/v +include-paths: + - mockstorage +categories: + - title: 'โ— Breaking Changes' + labels: + - 'โ— BreakingChange' + - title: '๐Ÿš€ New' + labels: + - 'โœ๏ธ Feature' + - title: '๐Ÿงน Updates' + labels: + - '๐Ÿงน Updates' + - '๐Ÿค– Dependencies' + - title: '๐Ÿ› Fixes' + labels: + - 'โ˜ข๏ธ Bug' + - title: '๐Ÿ“š Documentation' + labels: + - '๐Ÿ“’ Documentation' +change-template: '- $TITLE (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +exclude-contributors: + - dependabot + - dependabot[bot] +version-resolver: + major: + labels: + - 'major' + - 'โ— BreakingChange' + minor: + labels: + - 'minor' + - 'โœ๏ธ Feature' + patch: + labels: + - 'patch' + - '๐Ÿ“’ Documentation' + - 'โ˜ข๏ธ Bug' + - '๐Ÿค– Dependencies' + - '๐Ÿงน Updates' + default: patch +template: | + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...mockstorage/v$RESOLVED_VERSION + + Thank you $CONTRIBUTORS for making this update possible. From f088e43dced3cff59e0c207b7158c49b4ad5bb44 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 11:03:16 -0300 Subject: [PATCH 04/10] chore: update dependabot configuration for gomod package manifests in /mockstorage/ --- .github/dependabot.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9b055340..7e154f35 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -81,11 +81,11 @@ updates: schedule: interval: "daily" - package-ecosystem: "gomod" - directory: "/mockstorage/" # Location of package manifests - labels: - - "๐Ÿค– Dependencies" - schedule: - interval: "daily" + directory: "/mockstorage/" # Location of package manifests + labels: + - "๐Ÿค– Dependencies" + schedule: + interval: "daily" - package-ecosystem: "gomod" directory: "/mongodb/" # Location of package manifests labels: From ce553f5f1c7c1d43c83b9c9791d4fc1b17059efe Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 11:19:17 -0300 Subject: [PATCH 05/10] docs: improve README.md --- mockstorage/README.md | 111 +++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/mockstorage/README.md b/mockstorage/README.md index 28cd2216..63f31fcc 100644 --- a/mockstorage/README.md +++ b/mockstorage/README.md @@ -24,16 +24,32 @@ A mock storage implementation for Fiber. This storage is not persistent and is o ### Signatures ```go +// New creates a new Storage instance. You can optionally pass a Config. func New(config ...Config) *Storage + +// Get retrieves the value associated with the given key. func (s *Storage) Get(key string) ([]byte, error) + +// Set sets the value for the given key, with an optional expiration duration. func (s *Storage) Set(key string, val []byte, exp time.Duration) error + +// Delete removes the value associated with the given key. func (s *Storage) Delete(key string) error + +// Reset clears all values from the storage. func (s *Storage) Reset() error + +// Close performs any necessary cleanup when the storage is no longer needed. func (s *Storage) Close() error + +// Conn returns a copy of the current state of the storage. func (s *Storage) Conn() map[string]entry + +// Keys returns a list of all keys in the storage. func (s *Storage) Keys() ([][]byte, error) -func SetCustomFuncs(custom *CustomFuncs) +// SetCustomFuncs allows you to set custom functions for the storage operations. +func (s *Storage) SetCustomFuncs(custom *CustomFuncs) ``` ### Installation @@ -57,57 +73,52 @@ You can use the following possibilities to create a storage: // 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 - }, - // ... -}) +// Set a value in the storage. +err := store.Set("key1", []byte("value1"), 0) +if err != nil { + // handle error +} + +// Get a value from the storage. +val, err := store.Get("key1") +if err != nil { + // handle error +} +fmt.Println(string(val)) // prints "value1" + +// Delete a value from the storage. +err = store.Delete("key1") +if err != nil { + // handle error +} + +// Mocking storage operations in tests: +func TestMyFunction(t *testing.T) { + // Create a new instance of MockStorage + store := mockstorage.New() + + // Mock the Set function + store.SetCustomFuncs(&mockstorage.CustomFuncs{ + Set: func(key string, val []byte, exp time.Duration) error { + if key == "expectedKey" && string(val) == "expectedValue" { + return nil + } + return errors.New("unexpected key or value") + }, + }) + + // Call the function you want to test, which should call store.Set + err := MyFunction(store) + + // Check that the function behaved as expected + if err != nil { + t.Errorf("MyFunction returned an error: %v", err) + } +} ``` +> **Note:** In the `mockstorage` package, expiration of data is not handled automatically in the background. The data is only marked as expired and removed when you attempt to `Get()` it after its expiration time. If you're using a custom `Get()` function or accessing the data directly using the `Conn()` function, expired data will not be removed. Keep this in mind when writing your tests. + ### Config ```go type Config struct { From 1ab83b0150e8b2462d5dfa551756cf9df9b91d6b Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 11:29:55 -0300 Subject: [PATCH 06/10] feat: Update mockstorage package with Entry struct --- .../workflows/release-drafter-mockstorage.yml | 19 ++++++++++ mockstorage/README.md | 27 +++++++++++++- mockstorage/mockstorage.go | 28 +++++++-------- mockstorage/mockstorage_test.go | 36 +++++++++---------- 4 files changed, 77 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/release-drafter-mockstorage.yml diff --git a/.github/workflows/release-drafter-mockstorage.yml b/.github/workflows/release-drafter-mockstorage.yml new file mode 100644 index 00000000..9201acea --- /dev/null +++ b/.github/workflows/release-drafter-mockstorage.yml @@ -0,0 +1,19 @@ +name: Release Drafter MockStorage +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + - main + paths: + - 'mockstorage/**' +jobs: + draft_release_memcache: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter-memory.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/mockstorage/README.md b/mockstorage/README.md index 63f31fcc..062277b4 100644 --- a/mockstorage/README.md +++ b/mockstorage/README.md @@ -23,6 +23,31 @@ A mock storage implementation for Fiber. This storage is not persistent and is o ### Signatures + +#### Structs + +```go +type Storage struct { + // contains filtered or unexported fields +} + +type Entry struct { + Value []byte + Exp time.Time +} + +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) +} +``` + +#### Functions ```go // New creates a new Storage instance. You can optionally pass a Config. func New(config ...Config) *Storage @@ -43,7 +68,7 @@ func (s *Storage) Reset() error func (s *Storage) Close() error // Conn returns a copy of the current state of the storage. -func (s *Storage) Conn() map[string]entry +func (s *Storage) Conn() map[string]Entry // Keys returns a list of all keys in the storage. func (s *Storage) Keys() ([][]byte, error) diff --git a/mockstorage/mockstorage.go b/mockstorage/mockstorage.go index 9575a133..000757d8 100644 --- a/mockstorage/mockstorage.go +++ b/mockstorage/mockstorage.go @@ -14,14 +14,14 @@ type Config struct { // Storage is the mock storage adapter. type Storage struct { mu sync.RWMutex - data map[string]entry + data map[string]Entry custom *CustomFuncs } -// entry struct to hold value and expiration time. -type entry struct { - value []byte - exp time.Time +// Entry struct to hold value and expiration time. +type Entry struct { + Value []byte + Exp time.Time } // CustomFuncs allows injecting custom behaviors for testing. @@ -31,14 +31,14 @@ type CustomFuncs struct { DeleteFunc func(key string) error ResetFunc func() error CloseFunc func() error - ConnFunc func() map[string]entry + 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), + data: make(map[string]Entry), custom: &CustomFuncs{ GetFunc: nil, SetFunc: nil, @@ -71,11 +71,11 @@ func (s *Storage) Get(key string) ([]byte, error) { if !ok { return nil, errors.New("key not found") } - if !e.exp.IsZero() && time.Now().After(e.exp) { + if !e.Exp.IsZero() && time.Now().After(e.Exp) { delete(s.data, key) return nil, errors.New("key expired") } - return e.value, nil + return e.Value, nil } // Set sets the value for a given key with an expiration time. @@ -92,7 +92,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { expTime = time.Now().Add(exp) } - s.data[key] = entry{value: val, exp: expTime} + s.data[key] = Entry{Value: val, Exp: expTime} return nil } @@ -118,7 +118,7 @@ func (s *Storage) Reset() error { s.mu.Lock() defer s.mu.Unlock() - s.data = make(map[string]entry) + s.data = make(map[string]Entry) return nil } @@ -133,7 +133,7 @@ func (s *Storage) Close() error { } // Conn returns the internal data map (for testing purposes). -func (s *Storage) Conn() map[string]entry { +func (s *Storage) Conn() map[string]Entry { if s.custom.ConnFunc != nil { return s.custom.ConnFunc() } @@ -141,9 +141,9 @@ func (s *Storage) Conn() map[string]entry { s.mu.RLock() defer s.mu.RUnlock() - copyData := make(map[string]entry) + copyData := make(map[string]Entry) for k, v := range s.data { - copyData[k] = v + copyData[k] = Entry{Value: v.Value, Exp: v.Exp} } return copyData } diff --git a/mockstorage/mockstorage_test.go b/mockstorage/mockstorage_test.go index 8777cbe1..f5cbbf9d 100644 --- a/mockstorage/mockstorage_test.go +++ b/mockstorage/mockstorage_test.go @@ -68,10 +68,10 @@ 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)}, + 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)}, } }, } @@ -80,13 +80,13 @@ func TestStorageConnFunc(t *testing.T) { // 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)}, + 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) { + if val, ok := conn[k]; !ok || !bytes.Equal(val.Value, v.Value) { t.Errorf("Conn() = %v, want %v", conn, expectedConn) } } @@ -174,10 +174,10 @@ func TestStorageCustomBehavior(t *testing.T) { } 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)}, + 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) { @@ -225,13 +225,13 @@ func TestStorageCustomBehavior(t *testing.T) { // 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)}, + 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) { + if val, ok := conn[k]; !ok || !bytes.Equal(val.Value, v.Value) { t.Errorf("Conn() = %v, want %v", conn, expectedConn) } } @@ -261,8 +261,8 @@ func TestStorageConnAndKeys(t *testing.T) { 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{}}}) + 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 From 58d9dd4d4040e1e6a0039b82dc9ae2e287ec945f Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 11:30:28 -0300 Subject: [PATCH 07/10] chore: Update release-drafter-mockstorage.yml configuration --- .github/workflows/release-drafter-mockstorage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter-mockstorage.yml b/.github/workflows/release-drafter-mockstorage.yml index 9201acea..d31967ab 100644 --- a/.github/workflows/release-drafter-mockstorage.yml +++ b/.github/workflows/release-drafter-mockstorage.yml @@ -14,6 +14,6 @@ jobs: steps: - uses: release-drafter/release-drafter@v6 with: - config-name: release-drafter-memory.yml + config-name: release-drafter-mockstorage.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0fd13ff4dd80c272128325af7881b1b4c8fab30e Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 11:34:18 -0300 Subject: [PATCH 08/10] docs: Add Config struct to mockstorage package signatures --- mockstorage/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mockstorage/README.md b/mockstorage/README.md index 062277b4..e4b3216c 100644 --- a/mockstorage/README.md +++ b/mockstorage/README.md @@ -36,6 +36,10 @@ type Entry struct { Exp time.Time } +type Config struct { + CustomFuncs *CustomFuncs +} + type CustomFuncs struct { GetFunc func(key string) ([]byte, error) SetFunc func(key string, val []byte, exp time.Duration) error From e8f82908530be6012cb09e3438fc1ef5d33b0f05 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 4 Jun 2024 11:36:22 -0300 Subject: [PATCH 09/10] docs: Update mockstorage package README.md --- mockstorage/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mockstorage/README.md b/mockstorage/README.md index e4b3216c..89c0513c 100644 --- a/mockstorage/README.md +++ b/mockstorage/README.md @@ -14,7 +14,7 @@ A mock storage implementation for Fiber. This storage is not persistent and is o **Note: Requires Go 1.21 and above** -### Table of Contents +## Table of Contents - [Signatures](#signatures) - [Installation](#installation) - [Examples](#examples) @@ -22,9 +22,9 @@ A mock storage implementation for Fiber. This storage is not persistent and is o - [Default Config](#default-config) -### Signatures +## Signatures -#### Structs +### Structs ```go type Storage struct { @@ -51,7 +51,7 @@ type CustomFuncs struct { } ``` -#### Functions +### Functions ```go // New creates a new Storage instance. You can optionally pass a Config. func New(config ...Config) *Storage @@ -81,7 +81,7 @@ func (s *Storage) Keys() ([][]byte, error) func (s *Storage) SetCustomFuncs(custom *CustomFuncs) ``` -### Installation +## 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// @@ -91,7 +91,7 @@ And then install the mockstorage implementation: go get github.com/gofiber/storage/mockstorage ``` -### Examples +## Examples Import the storage package. ```go import "github.com/gofiber/storage/mockstorage" @@ -148,14 +148,14 @@ func TestMyFunction(t *testing.T) { > **Note:** In the `mockstorage` package, expiration of data is not handled automatically in the background. The data is only marked as expired and removed when you attempt to `Get()` it after its expiration time. If you're using a custom `Get()` function or accessing the data directly using the `Conn()` function, expired data will not be removed. Keep this in mind when writing your tests. -### Config +## Config ```go type Config struct { CustomFuncs *CustomFuncs } ``` -### Default Config +## Default Config ```go var ConfigDefault = Config{ CustomFuncs: &CustomFuncs{ From 91d99c4d77a89a3963dda569d708949d62caf4eb Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Wed, 5 Jun 2024 11:35:45 -0300 Subject: [PATCH 10/10] docs: Add MockStorage to root README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cc1759ed..532bebab 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ type Storage interface { - [Memcache](./memcache/README.md) - [Memory](./memory/README.md) - [Minio](./minio/README.md) +- [MockStorage](./mockstorage/README.md) - [MongoDB](./mongodb/README.md) - [MSSQL](./mssql/README.md) - [MySQL](./mysql/README.md)