store: add mock store
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
815
store/mock/mock.go
Normal file
815
store/mock/mock.go
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpectedWrite represents an expected Write operation
|
||||||
|
type ExpectedWrite struct {
|
||||||
|
key string
|
||||||
|
value interface{}
|
||||||
|
ttl time.Duration
|
||||||
|
metadata map[string]string
|
||||||
|
namespace string
|
||||||
|
times int
|
||||||
|
called int
|
||||||
|
mutex sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpectedWrite) match(key string, val interface{}, opts ...store.WriteOption) bool {
|
||||||
|
e.mutex.Lock()
|
||||||
|
defer e.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check key match
|
||||||
|
if e.key != "" && e.key != key {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check value match
|
||||||
|
if e.value != nil && !reflect.DeepEqual(e.value, val) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check options
|
||||||
|
options := store.NewWriteOptions(opts...)
|
||||||
|
if e.ttl > 0 && e.ttl != options.TTL {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e.namespace != "" && e.namespace != options.Namespace {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the expected times
|
||||||
|
if e.times > 0 && e.called >= e.times {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
e.called++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectedRead represents an expected Read operation
|
||||||
|
type ExpectedRead struct {
|
||||||
|
key string
|
||||||
|
value interface{}
|
||||||
|
times int
|
||||||
|
called int
|
||||||
|
mutex sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpectedRead) match(key string, opts ...store.ReadOption) bool {
|
||||||
|
e.mutex.Lock()
|
||||||
|
defer e.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check key match
|
||||||
|
if e.key != "" && e.key != key {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the expected times
|
||||||
|
if e.times > 0 && e.called >= e.times {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
e.called++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectedDelete represents an expected Delete operation
|
||||||
|
type ExpectedDelete struct {
|
||||||
|
key string
|
||||||
|
times int
|
||||||
|
called int
|
||||||
|
mutex sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpectedDelete) match(key string, opts ...store.DeleteOption) bool {
|
||||||
|
e.mutex.Lock()
|
||||||
|
defer e.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check key match
|
||||||
|
if e.key != "" && e.key != key {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the expected times
|
||||||
|
if e.times > 0 && e.called >= e.times {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
e.called++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectedExists represents an expected Exists operation
|
||||||
|
type ExpectedExists struct {
|
||||||
|
key string
|
||||||
|
times int
|
||||||
|
called int
|
||||||
|
mutex sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpectedExists) match(key string, opts ...store.ExistsOption) bool {
|
||||||
|
e.mutex.Lock()
|
||||||
|
defer e.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check key match
|
||||||
|
if e.key != "" && e.key != key {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the expected times
|
||||||
|
if e.times > 0 && e.called >= e.times {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
e.called++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectedList represents an expected List operation
|
||||||
|
type ExpectedList struct {
|
||||||
|
times int
|
||||||
|
called int
|
||||||
|
mutex sync.Mutex
|
||||||
|
err error
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpectedList) match(opts ...store.ListOption) bool {
|
||||||
|
e.mutex.Lock()
|
||||||
|
defer e.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check if we've exceeded the expected times
|
||||||
|
if e.times > 0 && e.called >= e.times {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
e.called++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store is a mock implementation of the Store interface for testing
|
||||||
|
type Store struct {
|
||||||
|
expectedWrites []*ExpectedWrite
|
||||||
|
expectedReads []*ExpectedRead
|
||||||
|
expectedDeletes []*ExpectedDelete
|
||||||
|
expectedExists []*ExpectedExists
|
||||||
|
expectedLists []*ExpectedList
|
||||||
|
|
||||||
|
data map[string]interface{}
|
||||||
|
exists map[string]bool
|
||||||
|
ttls map[string]time.Time // key -> expiration time
|
||||||
|
metadata map[string]map[string]string
|
||||||
|
err error
|
||||||
|
opts store.Options
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStore creates a new mock store
|
||||||
|
func NewStore(opts ...store.Option) *Store {
|
||||||
|
options := store.NewOptions(opts...)
|
||||||
|
return &Store{
|
||||||
|
data: make(map[string]interface{}),
|
||||||
|
exists: make(map[string]bool),
|
||||||
|
ttls: make(map[string]time.Time),
|
||||||
|
metadata: make(map[string]map[string]string),
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectWrite creates an expectation for a Write operation
|
||||||
|
func (m *Store) ExpectWrite(key string) *ExpectedWrite {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
exp := &ExpectedWrite{key: key}
|
||||||
|
m.expectedWrites = append(m.expectedWrites, exp)
|
||||||
|
return exp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectRead creates an expectation for a Read operation
|
||||||
|
func (m *Store) ExpectRead(key string) *ExpectedRead {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
exp := &ExpectedRead{key: key}
|
||||||
|
m.expectedReads = append(m.expectedReads, exp)
|
||||||
|
return exp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectDelete creates an expectation for a Delete operation
|
||||||
|
func (m *Store) ExpectDelete(key string) *ExpectedDelete {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
exp := &ExpectedDelete{key: key}
|
||||||
|
m.expectedDeletes = append(m.expectedDeletes, exp)
|
||||||
|
return exp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectExists creates an expectation for an Exists operation
|
||||||
|
func (m *Store) ExpectExists(key string) *ExpectedExists {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
exp := &ExpectedExists{key: key}
|
||||||
|
m.expectedExists = append(m.expectedExists, exp)
|
||||||
|
return exp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectList creates an expectation for a List operation
|
||||||
|
func (m *Store) ExpectList() *ExpectedList {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
exp := &ExpectedList{}
|
||||||
|
m.expectedLists = append(m.expectedLists, exp)
|
||||||
|
return exp
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValue sets the value to return for expected operations
|
||||||
|
func (e *ExpectedWrite) WithValue(val interface{}) *ExpectedWrite {
|
||||||
|
e.value = val
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTTL sets the TTL for expected Write operations
|
||||||
|
func (e *ExpectedWrite) WithTTL(ttl time.Duration) *ExpectedWrite {
|
||||||
|
e.ttl = ttl
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNamespace sets the namespace for expected operations
|
||||||
|
func (e *ExpectedWrite) WithNamespace(ns string) *ExpectedWrite {
|
||||||
|
e.namespace = ns
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times sets how many times the expectation should be called
|
||||||
|
func (e *ExpectedWrite) Times(n int) *ExpectedWrite {
|
||||||
|
e.times = n
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillReturnError sets an error to return for the expected operation
|
||||||
|
func (e *ExpectedWrite) WillReturnError(err error) *ExpectedWrite {
|
||||||
|
e.err = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValue sets the value to return for expected Read operations
|
||||||
|
func (e *ExpectedRead) WithValue(val interface{}) *ExpectedRead {
|
||||||
|
e.value = val
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times sets how many times the expectation should be called
|
||||||
|
func (e *ExpectedRead) Times(n int) *ExpectedRead {
|
||||||
|
e.times = n
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillReturnError sets an error to return for the expected operation
|
||||||
|
func (e *ExpectedRead) WillReturnError(err error) *ExpectedRead {
|
||||||
|
e.err = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times sets how many times the expectation should be called
|
||||||
|
func (e *ExpectedDelete) Times(n int) *ExpectedDelete {
|
||||||
|
e.times = n
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillReturnError sets an error to return for the expected operation
|
||||||
|
func (e *ExpectedDelete) WillReturnError(err error) *ExpectedDelete {
|
||||||
|
e.err = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times sets how many times the expectation should be called
|
||||||
|
func (e *ExpectedExists) Times(n int) *ExpectedExists {
|
||||||
|
e.times = n
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillReturnError sets an error to return for the expected operation
|
||||||
|
func (e *ExpectedExists) WillReturnError(err error) *ExpectedExists {
|
||||||
|
e.err = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillReturn sets the keys to return for List operations
|
||||||
|
func (e *ExpectedList) WillReturn(keys ...string) *ExpectedList {
|
||||||
|
e.keys = keys
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times sets how many times the expectation should be called
|
||||||
|
func (e *ExpectedList) Times(n int) *ExpectedList {
|
||||||
|
e.times = n
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillReturnError sets an error to return for the expected operation
|
||||||
|
func (e *ExpectedList) WillReturnError(err error) *ExpectedList {
|
||||||
|
e.err = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTTL checks if a key has expired
|
||||||
|
func (m *Store) checkTTL(key string) bool {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
if exp, ok := m.ttls[key]; ok {
|
||||||
|
if time.Now().After(exp) {
|
||||||
|
delete(m.data, key)
|
||||||
|
delete(m.exists, key)
|
||||||
|
delete(m.ttls, key)
|
||||||
|
delete(m.metadata, key)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// FastForward decrements all TTLs by the given duration
|
||||||
|
func (m *Store) FastForward(d time.Duration) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for key, exp := range m.ttls {
|
||||||
|
// Calculate remaining time before fast forward
|
||||||
|
remaining := time.Until(exp)
|
||||||
|
if remaining <= 0 {
|
||||||
|
// Already expired, remove it
|
||||||
|
delete(m.data, key)
|
||||||
|
delete(m.exists, key)
|
||||||
|
delete(m.ttls, key)
|
||||||
|
delete(m.metadata, key)
|
||||||
|
} else {
|
||||||
|
// Apply fast forward
|
||||||
|
newRemaining := remaining - d
|
||||||
|
if newRemaining <= 0 {
|
||||||
|
// Would expire after fast forward, remove it
|
||||||
|
delete(m.data, key)
|
||||||
|
delete(m.exists, key)
|
||||||
|
delete(m.ttls, key)
|
||||||
|
delete(m.metadata, key)
|
||||||
|
} else {
|
||||||
|
// Update expiration time
|
||||||
|
m.ttls[key] = now.Add(newRemaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns store name
|
||||||
|
func (m *Store) Name() string {
|
||||||
|
return m.opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the mock store
|
||||||
|
func (m *Store) Init(opts ...store.Option) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&m.opts)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect is used when store needs to be connected
|
||||||
|
func (m *Store) Connect(ctx context.Context) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options returns the current options
|
||||||
|
func (m *Store) Options() store.Options {
|
||||||
|
return m.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists checks that key exists in store
|
||||||
|
func (m *Store) Exists(ctx context.Context, key string, opts ...store.ExistsOption) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check TTL first
|
||||||
|
if !m.checkTTL(key) {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching expectation
|
||||||
|
m.mutex.Lock()
|
||||||
|
for _, exp := range m.expectedExists {
|
||||||
|
if exp.match(key, opts...) {
|
||||||
|
m.mutex.Unlock()
|
||||||
|
if exp.err != nil {
|
||||||
|
return exp.err
|
||||||
|
}
|
||||||
|
if !m.exists[key] {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
// If no expectation matched, use default behavior
|
||||||
|
if !m.exists[key] {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads a single key name to provided value with optional ReadOptions
|
||||||
|
func (m *Store) Read(ctx context.Context, key string, val interface{}, opts ...store.ReadOption) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check TTL first
|
||||||
|
if !m.checkTTL(key) {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching expectation
|
||||||
|
m.mutex.Lock()
|
||||||
|
for _, exp := range m.expectedReads {
|
||||||
|
if exp.match(key, opts...) {
|
||||||
|
m.mutex.Unlock()
|
||||||
|
if exp.err != nil {
|
||||||
|
return exp.err
|
||||||
|
}
|
||||||
|
if !m.exists[key] {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the value from expected or actual data
|
||||||
|
data := exp.value
|
||||||
|
if data == nil {
|
||||||
|
data = m.data[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if data != nil {
|
||||||
|
// Simple type conversion for testing
|
||||||
|
if target, ok := val.(*interface{}); ok {
|
||||||
|
*target = data
|
||||||
|
} else if target, ok := val.(*string); ok {
|
||||||
|
if s, ok := data.(string); ok {
|
||||||
|
*target = s
|
||||||
|
} else {
|
||||||
|
*target = fmt.Sprintf("%v", data)
|
||||||
|
}
|
||||||
|
} else if target, ok := val.(*int); ok {
|
||||||
|
if i, ok := data.(int); ok {
|
||||||
|
*target = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
// If no expectation matched, use default behavior
|
||||||
|
if !m.exists[key] {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, ok := m.data[key]; ok {
|
||||||
|
if target, ok := val.(*interface{}); ok {
|
||||||
|
*target = data
|
||||||
|
} else if target, ok := val.(*string); ok {
|
||||||
|
if s, ok := data.(string); ok {
|
||||||
|
*target = s
|
||||||
|
} else {
|
||||||
|
*target = fmt.Sprintf("%v", data)
|
||||||
|
}
|
||||||
|
} else if target, ok := val.(*int); ok {
|
||||||
|
if i, ok := data.(int); ok {
|
||||||
|
*target = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes a value to key name to the store with optional WriteOption
|
||||||
|
func (m *Store) Write(ctx context.Context, key string, val interface{}, opts ...store.WriteOption) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching expectation
|
||||||
|
m.mutex.Lock()
|
||||||
|
for _, exp := range m.expectedWrites {
|
||||||
|
if exp.match(key, val, opts...) {
|
||||||
|
m.mutex.Unlock()
|
||||||
|
if exp.err != nil {
|
||||||
|
return exp.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the write operation
|
||||||
|
m.mutex.Lock()
|
||||||
|
m.data[key] = val
|
||||||
|
m.exists[key] = true
|
||||||
|
|
||||||
|
// Handle TTL
|
||||||
|
options := store.NewWriteOptions(opts...)
|
||||||
|
if options.TTL > 0 {
|
||||||
|
m.ttls[key] = time.Now().Add(options.TTL)
|
||||||
|
} else {
|
||||||
|
delete(m.ttls, key) // Remove TTL if not set
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle metadata
|
||||||
|
if options.Metadata != nil {
|
||||||
|
m.metadata[key] = make(map[string]string)
|
||||||
|
for k, v := range options.Metadata {
|
||||||
|
// Convert []string to string by joining with comma
|
||||||
|
if len(v) > 0 {
|
||||||
|
m.metadata[key][k] = strings.Join(v, ",")
|
||||||
|
} else {
|
||||||
|
m.metadata[key][k] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
// If no expectation matched, use default behavior
|
||||||
|
m.mutex.Lock()
|
||||||
|
m.data[key] = val
|
||||||
|
m.exists[key] = true
|
||||||
|
|
||||||
|
options := store.NewWriteOptions(opts...)
|
||||||
|
if options.TTL > 0 {
|
||||||
|
m.ttls[key] = time.Now().Add(options.TTL)
|
||||||
|
} else {
|
||||||
|
delete(m.ttls, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Metadata != nil {
|
||||||
|
m.metadata[key] = make(map[string]string)
|
||||||
|
for k, v := range options.Metadata {
|
||||||
|
// Convert []string to string by joining with comma
|
||||||
|
if len(v) > 0 {
|
||||||
|
m.metadata[key][k] = strings.Join(v, ",")
|
||||||
|
} else {
|
||||||
|
m.metadata[key][k] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the record with the corresponding key from the store
|
||||||
|
func (m *Store) Delete(ctx context.Context, key string, opts ...store.DeleteOption) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching expectation
|
||||||
|
m.mutex.Lock()
|
||||||
|
for _, exp := range m.expectedDeletes {
|
||||||
|
if exp.match(key, opts...) {
|
||||||
|
m.mutex.Unlock()
|
||||||
|
if exp.err != nil {
|
||||||
|
return exp.err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
delete(m.data, key)
|
||||||
|
delete(m.exists, key)
|
||||||
|
delete(m.ttls, key)
|
||||||
|
delete(m.metadata, key)
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
// If no expectation matched, use default behavior
|
||||||
|
m.mutex.Lock()
|
||||||
|
delete(m.data, key)
|
||||||
|
delete(m.exists, key)
|
||||||
|
delete(m.ttls, key)
|
||||||
|
delete(m.metadata, key)
|
||||||
|
m.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns any keys that match, or an empty list with no error if none matched
|
||||||
|
func (m *Store) List(ctx context.Context, opts ...store.ListOption) ([]string, error) {
|
||||||
|
if m.err != nil {
|
||||||
|
return nil, m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching expectation
|
||||||
|
m.mutex.Lock()
|
||||||
|
for _, exp := range m.expectedLists {
|
||||||
|
if exp.match(opts...) {
|
||||||
|
m.mutex.Unlock()
|
||||||
|
if exp.err != nil {
|
||||||
|
return nil, exp.err
|
||||||
|
}
|
||||||
|
return exp.keys, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
// If no expectation matched, return actual keys
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
for key := range m.data {
|
||||||
|
// Check TTL
|
||||||
|
if exp, ok := m.ttls[key]; ok {
|
||||||
|
if time.Now().After(exp) {
|
||||||
|
continue // Skip expired keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply list options filtering
|
||||||
|
options := store.NewListOptions(opts...)
|
||||||
|
if options.Prefix != "" {
|
||||||
|
var filtered []string
|
||||||
|
for _, key := range keys {
|
||||||
|
if len(key) >= len(options.Prefix) && key[:len(options.Prefix)] == options.Prefix {
|
||||||
|
filtered = append(filtered, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Suffix != "" {
|
||||||
|
var filtered []string
|
||||||
|
for _, key := range keys {
|
||||||
|
if len(key) >= len(options.Suffix) && key[len(key)-len(options.Suffix):] == options.Suffix {
|
||||||
|
filtered = append(filtered, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply limit and offset
|
||||||
|
if options.Limit > 0 && int(options.Limit) < len(keys) {
|
||||||
|
end := int(options.Offset) + int(options.Limit)
|
||||||
|
if end > len(keys) {
|
||||||
|
end = len(keys)
|
||||||
|
}
|
||||||
|
if int(options.Offset) < len(keys) {
|
||||||
|
keys = keys[options.Offset:end]
|
||||||
|
} else {
|
||||||
|
keys = []string{}
|
||||||
|
}
|
||||||
|
} else if options.Offset > 0 && int(options.Offset) < len(keys) {
|
||||||
|
keys = keys[options.Offset:]
|
||||||
|
} else if options.Offset >= uint(len(keys)) {
|
||||||
|
keys = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect disconnects the mock store
|
||||||
|
func (m *Store) Disconnect(ctx context.Context) error {
|
||||||
|
if m.err != nil {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the implementation
|
||||||
|
func (m *Store) String() string {
|
||||||
|
return "mock"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch returns events watcher
|
||||||
|
func (m *Store) Watch(ctx context.Context, opts ...store.WatchOption) (store.Watcher, error) {
|
||||||
|
if m.err != nil {
|
||||||
|
return nil, m.err
|
||||||
|
}
|
||||||
|
return NewWatcher(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live returns store liveness
|
||||||
|
func (m *Store) Live() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ready returns store readiness
|
||||||
|
func (m *Store) Ready() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health returns store health
|
||||||
|
func (m *Store) Health() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectationsWereMet checks that all expected operations were called the expected number of times
|
||||||
|
func (m *Store) ExpectationsWereMet() error {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
for _, exp := range m.expectedWrites {
|
||||||
|
if exp.times > 0 && exp.called != exp.times {
|
||||||
|
return fmt.Errorf("expected write for key %s to be called %d times, but was called %d times", exp.key, exp.times, exp.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exp := range m.expectedReads {
|
||||||
|
if exp.times > 0 && exp.called != exp.times {
|
||||||
|
return fmt.Errorf("expected read for key %s to be called %d times, but was called %d times", exp.key, exp.times, exp.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exp := range m.expectedDeletes {
|
||||||
|
if exp.times > 0 && exp.called != exp.times {
|
||||||
|
return fmt.Errorf("expected delete for key %s to be called %d times, but was called %d times", exp.key, exp.times, exp.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exp := range m.expectedExists {
|
||||||
|
if exp.times > 0 && exp.called != exp.times {
|
||||||
|
return fmt.Errorf("expected exists for key %s to be called %d times, but was called %d times", exp.key, exp.times, exp.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exp := range m.expectedLists {
|
||||||
|
if exp.times > 0 && exp.called != exp.times {
|
||||||
|
return fmt.Errorf("expected list to be called %d times, but was called %d times", exp.times, exp.called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watcher is a mock implementation of the Watcher interface
|
||||||
|
type Watcher struct {
|
||||||
|
events chan store.Event
|
||||||
|
stop chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a new mock watcher
|
||||||
|
func NewWatcher() *Watcher {
|
||||||
|
return &Watcher{
|
||||||
|
events: make(chan store.Event, 1),
|
||||||
|
stop: make(chan bool, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next is a blocking call that returns the next event
|
||||||
|
func (mw *Watcher) Next() (store.Event, error) {
|
||||||
|
select {
|
||||||
|
case event := <-mw.events:
|
||||||
|
return event, nil
|
||||||
|
case <-mw.stop:
|
||||||
|
return nil, store.ErrWatcherStopped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the watcher
|
||||||
|
func (mw *Watcher) Stop() {
|
||||||
|
select {
|
||||||
|
case mw.stop <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent sends an event to the watcher (for testing purposes)
|
||||||
|
func (mw *Watcher) SendEvent(event store.Event) {
|
||||||
|
select {
|
||||||
|
case mw.events <- event:
|
||||||
|
default:
|
||||||
|
// If channel is full, drop the event
|
||||||
|
}
|
||||||
|
}
|
||||||
295
store/mock/mock_test.go
Normal file
295
store/mock/mock_test.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := NewStore()
|
||||||
|
|
||||||
|
// Test Write with expectation
|
||||||
|
s.ExpectWrite("test_key").WithValue("test_value")
|
||||||
|
err := s.Write(ctx, "test_key", "test_value")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Read with expectation
|
||||||
|
s.ExpectRead("test_key").WithValue("test_value")
|
||||||
|
var value interface{}
|
||||||
|
err = s.Read(ctx, "test_key", &value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read failed: %v", err)
|
||||||
|
}
|
||||||
|
if value != "test_value" {
|
||||||
|
t.Fatalf("Expected 'test_value', got %v", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Read with string
|
||||||
|
s.ExpectRead("test_key")
|
||||||
|
var strValue string
|
||||||
|
err = s.Read(ctx, "test_key", &strValue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read string failed: %v", err)
|
||||||
|
}
|
||||||
|
if strValue != "test_value" {
|
||||||
|
t.Fatalf("Expected 'test_value', got %s", strValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Write and Read integer with TTL
|
||||||
|
s.ExpectWrite("int_key").WithValue(42).WithTTL(5 * time.Second)
|
||||||
|
err = s.Write(ctx, "int_key", 42, store.WriteTTL(5*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write int failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ExpectRead("int_key")
|
||||||
|
var intValue int
|
||||||
|
err = s.Read(ctx, "int_key", &intValue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read int failed: %v", err)
|
||||||
|
}
|
||||||
|
if intValue != 42 {
|
||||||
|
t.Fatalf("Expected 42, got %d", intValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Exists with expectation
|
||||||
|
s.ExpectExists("test_key")
|
||||||
|
err = s.Exists(ctx, "test_key")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Exists failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test List with expectation
|
||||||
|
s.ExpectList().WillReturn("test_key", "another_key")
|
||||||
|
keys, err := s.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(keys) != 2 {
|
||||||
|
t.Fatalf("Expected 2 keys, got %d", len(keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Delete with expectation
|
||||||
|
s.ExpectDelete("test_key")
|
||||||
|
err = s.Delete(ctx, "test_key")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that deleted key doesn't exist
|
||||||
|
s.ExpectExists("test_key").WillReturnError(store.ErrNotFound)
|
||||||
|
err = s.Exists(ctx, "test_key")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected store.ErrNotFound after delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test error handling
|
||||||
|
s.ExpectExists("nonexistent").WillReturnError(store.ErrNotFound)
|
||||||
|
err = s.Exists(ctx, "nonexistent")
|
||||||
|
if err != store.ErrNotFound {
|
||||||
|
t.Fatalf("Expected store.ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all expectations were met
|
||||||
|
if err := s.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("Expectations not met: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreFastForward(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := NewStore()
|
||||||
|
|
||||||
|
// Write with TTL
|
||||||
|
s.ExpectWrite("ttl_key").WithValue("ttl_value").WithTTL(100 * time.Millisecond)
|
||||||
|
err := s.Write(ctx, "ttl_key", "ttl_value", store.WriteTTL(100*time.Millisecond))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write with TTL failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check key exists before TTL expires
|
||||||
|
s.ExpectRead("ttl_key")
|
||||||
|
var value string
|
||||||
|
err = s.Read(ctx, "ttl_key", &value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read before TTL failed: %v", err)
|
||||||
|
}
|
||||||
|
if value != "ttl_value" {
|
||||||
|
t.Fatalf("Expected 'ttl_value', got %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast forward by 50ms - key should still exist
|
||||||
|
s.FastForward(50 * time.Millisecond)
|
||||||
|
|
||||||
|
s.ExpectRead("ttl_key")
|
||||||
|
err = s.Read(ctx, "ttl_key", &value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read after 50ms fast forward failed: %v", err)
|
||||||
|
}
|
||||||
|
if value != "ttl_value" {
|
||||||
|
t.Fatalf("Expected 'ttl_value' after 50ms, got %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast forward by another 60ms (total 110ms) - key should expire
|
||||||
|
s.FastForward(60 * time.Millisecond)
|
||||||
|
|
||||||
|
s.ExpectRead("ttl_key").WillReturnError(store.ErrNotFound)
|
||||||
|
err = s.Read(ctx, "ttl_key", &value)
|
||||||
|
if err != store.ErrNotFound {
|
||||||
|
t.Fatalf("Expected store.ErrNotFound after TTL, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test FastForward on already expired keys
|
||||||
|
s.ExpectWrite("ttl_key2").WithValue("ttl_value2").WithTTL(10 * time.Millisecond)
|
||||||
|
err = s.Write(ctx, "ttl_key2", "ttl_value2", store.WriteTTL(10*time.Millisecond))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write with TTL failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast forward by 20ms - key should expire immediately
|
||||||
|
s.FastForward(20 * time.Millisecond)
|
||||||
|
|
||||||
|
s.ExpectRead("ttl_key2").WillReturnError(store.ErrNotFound)
|
||||||
|
err = s.Read(ctx, "ttl_key2", &value)
|
||||||
|
if err != store.ErrNotFound {
|
||||||
|
t.Fatalf("Expected store.ErrNotFound after immediate expiration, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("Expectations not met: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreWithOptions(t *testing.T) {
|
||||||
|
s := NewStore(store.Name("test_mock"), store.Namespace("test_ns"))
|
||||||
|
|
||||||
|
if s.Name() != "test_mock" {
|
||||||
|
t.Fatalf("Expected name 'test_mock', got %s", s.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := s.Options()
|
||||||
|
if opts.Namespace != "test_ns" {
|
||||||
|
t.Fatalf("Expected namespace 'test_ns', got %s", opts.Namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatcher(t *testing.T) {
|
||||||
|
watcher := NewWatcher()
|
||||||
|
|
||||||
|
// Test Stop
|
||||||
|
watcher.Stop()
|
||||||
|
|
||||||
|
// Test Next after stop
|
||||||
|
_, err := watcher.Next()
|
||||||
|
if err != store.ErrWatcherStopped {
|
||||||
|
t.Fatalf("Expected store.ErrWatcherStopped, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreHealth(t *testing.T) {
|
||||||
|
s := NewStore()
|
||||||
|
|
||||||
|
if !s.Live() {
|
||||||
|
t.Fatal("Expected Live() to return true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Ready() {
|
||||||
|
t.Fatal("Expected Ready() to return true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Health() {
|
||||||
|
t.Fatal("Expected Health() to return true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreConnectDisconnect(t *testing.T) {
|
||||||
|
s := NewStore()
|
||||||
|
|
||||||
|
err := s.Connect(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Connect failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Disconnect(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Disconnect failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test error propagation
|
||||||
|
s.ExpectWrite("test_key").WillReturnError(store.ErrNotConnected)
|
||||||
|
err = s.Write(context.Background(), "test_key", "value")
|
||||||
|
if err != store.ErrNotConnected {
|
||||||
|
t.Fatalf("Expected store.ErrNotConnected, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreTTL(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := NewStore()
|
||||||
|
|
||||||
|
// Test Write with TTL
|
||||||
|
s.ExpectWrite("ttl_key").WithValue("ttl_value").WithTTL(100 * time.Millisecond)
|
||||||
|
err := s.Write(ctx, "ttl_key", "ttl_value", store.WriteTTL(100*time.Millisecond))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write with TTL failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read before TTL expires
|
||||||
|
s.ExpectRead("ttl_key")
|
||||||
|
var value string
|
||||||
|
err = s.Read(ctx, "ttl_key", &value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read before TTL failed: %v", err)
|
||||||
|
}
|
||||||
|
if value != "ttl_value" {
|
||||||
|
t.Fatalf("Expected 'ttl_value', got %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for TTL to expire
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
// Read after TTL expires should return ErrNotFound
|
||||||
|
s.ExpectRead("ttl_key").WillReturnError(store.ErrNotFound)
|
||||||
|
err = s.Read(ctx, "ttl_key", &value)
|
||||||
|
if err != store.ErrNotFound {
|
||||||
|
t.Fatalf("Expected store.ErrNotFound after TTL, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("Expectations not met: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreExpectedOperations(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := NewStore()
|
||||||
|
|
||||||
|
// Test expected operations with Times
|
||||||
|
s.ExpectWrite("once_key").Times(1)
|
||||||
|
s.ExpectWrite("twice_key").Times(2)
|
||||||
|
|
||||||
|
err := s.Write(ctx, "once_key", "value1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Write(ctx, "twice_key", "value2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Write(ctx, "twice_key", "value3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Write failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("Expectations not met: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user