kevo/pkg/replication/manager_test.go
Jeremy Tregunna f9e332096c feat: Update client sdk (Go) with smart connection logic
- Client SDK will connect to a node, get node information and decide if
  it needs to connect to a primary for writes, or pick a replica to
  connect to for reads
- Updated service with a GetNodeInfo rpc call which returns information
  about the node to enable the smart selection code in the sdks
2025-04-29 15:03:03 -06:00

251 lines
5.6 KiB
Go

package replication
import (
"testing"
"github.com/KevoDB/kevo/pkg/common/iterator"
"github.com/KevoDB/kevo/pkg/engine/interfaces"
"github.com/KevoDB/kevo/pkg/wal"
)
// MockEngine implements a minimal mock engine for testing
type MockEngine struct {
wal *wal.WAL
readOnly bool
}
// Implement only essential methods for the test
func (m *MockEngine) GetWAL() *wal.WAL {
return m.wal
}
func (m *MockEngine) SetReadOnly(readOnly bool) {
m.readOnly = readOnly
}
func (m *MockEngine) IsReadOnly() bool {
return m.readOnly
}
func (m *MockEngine) FlushImMemTables() error {
return nil
}
// Implement required interface methods with minimal stubs
func (m *MockEngine) Put(key, value []byte) error {
return nil
}
func (m *MockEngine) Get(key []byte) ([]byte, error) {
return nil, nil
}
func (m *MockEngine) Delete(key []byte) error {
return nil
}
func (m *MockEngine) IsDeleted(key []byte) (bool, error) {
return false, nil
}
func (m *MockEngine) GetIterator() (iterator.Iterator, error) {
return nil, nil
}
func (m *MockEngine) GetRangeIterator(startKey, endKey []byte) (iterator.Iterator, error) {
return nil, nil
}
func (m *MockEngine) ApplyBatch(entries []*wal.Entry) error {
return nil
}
func (m *MockEngine) BeginTransaction(readOnly bool) (interfaces.Transaction, error) {
return nil, nil
}
func (m *MockEngine) TriggerCompaction() error {
return nil
}
func (m *MockEngine) CompactRange(startKey, endKey []byte) error {
return nil
}
func (m *MockEngine) GetStats() map[string]interface{} {
return map[string]interface{}{}
}
func (m *MockEngine) GetCompactionStats() (map[string]interface{}, error) {
return map[string]interface{}{}, nil
}
func (m *MockEngine) Close() error {
return nil
}
// TestNewManager tests the creation of a new replication manager
func TestNewManager(t *testing.T) {
engine := &MockEngine{}
// Test with nil config
manager, err := NewManager(engine, nil)
if err != nil {
t.Fatalf("Expected no error when creating manager with nil config, got: %v", err)
}
if manager == nil {
t.Fatal("Expected non-nil manager")
}
if manager.config.Enabled {
t.Error("Expected Enabled to be false")
}
if manager.config.Mode != "standalone" {
t.Errorf("Expected Mode to be 'standalone', got '%s'", manager.config.Mode)
}
// Test with custom config
config := &ManagerConfig{
Enabled: true,
Mode: "primary",
ListenAddr: ":50053",
PrimaryAddr: "localhost:50053",
}
manager, err = NewManager(engine, config)
if err != nil {
t.Fatalf("Expected no error when creating manager with custom config, got: %v", err)
}
if manager == nil {
t.Fatal("Expected non-nil manager")
}
if !manager.config.Enabled {
t.Error("Expected Enabled to be true")
}
if manager.config.Mode != "primary" {
t.Errorf("Expected Mode to be 'primary', got '%s'", manager.config.Mode)
}
}
// TestManagerStartStandalone tests starting the manager in standalone mode
func TestManagerStartStandalone(t *testing.T) {
engine := &MockEngine{}
config := &ManagerConfig{
Enabled: true,
Mode: "standalone",
}
manager, err := NewManager(engine, config)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
err = manager.Start()
if err != nil {
t.Errorf("Expected no error when starting in standalone mode, got: %v", err)
}
if manager.serviceStatus {
t.Error("Expected serviceStatus to be false")
}
err = manager.Stop()
if err != nil {
t.Errorf("Expected no error when stopping, got: %v", err)
}
}
// TestManagerStatus tests the status reporting functionality
func TestManagerStatus(t *testing.T) {
engine := &MockEngine{}
// Test disabled mode
config := &ManagerConfig{
Enabled: false,
Mode: "standalone",
}
manager, _ := NewManager(engine, config)
status := manager.Status()
if status["enabled"].(bool) != false {
t.Error("Expected 'enabled' to be false")
}
if status["mode"].(string) != "standalone" {
t.Errorf("Expected 'mode' to be 'standalone', got '%s'", status["mode"].(string))
}
if status["active"].(bool) != false {
t.Error("Expected 'active' to be false")
}
// Test primary mode
config = &ManagerConfig{
Enabled: true,
Mode: "primary",
ListenAddr: ":50057",
}
manager, _ = NewManager(engine, config)
manager.serviceStatus = true
status = manager.Status()
if status["enabled"].(bool) != true {
t.Error("Expected 'enabled' to be true")
}
if status["mode"].(string) != "primary" {
t.Errorf("Expected 'mode' to be 'primary', got '%s'", status["mode"].(string))
}
if status["active"].(bool) != true {
t.Error("Expected 'active' to be true")
}
// There will be no listen_address in the status until the primary is actually created
// so we skip checking that field
}
// TestEngineApplier tests the engine applier implementation
func TestEngineApplier(t *testing.T) {
engine := &MockEngine{}
applier := NewEngineApplier(engine)
// Test Put
entry := &wal.Entry{
Type: wal.OpTypePut,
Key: []byte("test-key"),
Value: []byte("test-value"),
}
err := applier.Apply(entry)
if err != nil {
t.Errorf("Expected no error for Put, got: %v", err)
}
// Test Delete
entry = &wal.Entry{
Type: wal.OpTypeDelete,
Key: []byte("test-key"),
}
err = applier.Apply(entry)
if err != nil {
t.Errorf("Expected no error for Delete, got: %v", err)
}
// Test Batch
entry = &wal.Entry{
Type: wal.OpTypeBatch,
Key: []byte("test-key"),
}
err = applier.Apply(entry)
if err != nil {
t.Errorf("Expected no error for Batch, got: %v", err)
}
// Test unsupported type
entry = &wal.Entry{
Type: 99, // Invalid type
Key: []byte("test-key"),
}
err = applier.Apply(entry)
if err == nil {
t.Error("Expected error for unsupported entry type")
}
}