feat: add mockstorage

This commit is contained in:
Jason McNeil
2024-06-03 15:22:45 -03:00
parent 442c85f4ea
commit 2ce70bcb0e
6 changed files with 608 additions and 0 deletions

28
.github/workflows/test-mockstorage.yml vendored Normal file
View File

@@ -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

131
mockstorage/README.md Normal file
View File

@@ -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/<user>/<repo>
```
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,
},
}
```

3
mockstorage/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module mockstorage
go 1.21

0
mockstorage/go.sum Normal file
View File

170
mockstorage/mockstorage.go Normal file
View File

@@ -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
}

View File

@@ -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")})
}
}