From 0b41a6011211db1e94ea6de7f65bfb83dd23eb9c Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Wed, 10 Dec 2025 01:35:30 +0300 Subject: [PATCH] store: add mock store Signed-off-by: Vasiliy Tolstov --- store/mock/mock.go | 815 ++++++++++++++++++++++++++++++++++++++++ store/mock/mock_test.go | 295 +++++++++++++++ 2 files changed, 1110 insertions(+) create mode 100644 store/mock/mock.go create mode 100644 store/mock/mock_test.go diff --git a/store/mock/mock.go b/store/mock/mock.go new file mode 100644 index 00000000..cfbb5c2d --- /dev/null +++ b/store/mock/mock.go @@ -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 + } +} diff --git a/store/mock/mock_test.go b/store/mock/mock_test.go new file mode 100644 index 00000000..4f6d5d3e --- /dev/null +++ b/store/mock/mock_test.go @@ -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) + } +}