package openstack import ( "context" "fmt" "sync" ) // MockClient is an in-memory FloatingIPClient used by unit tests and the // offline end-to-end harness. Seed it with the pool of floating IPs the // scenario expects to exist before use. type MockClient struct { mu sync.Mutex fips map[string]*FloatingIP // keyed by ID byIP map[string]string // address -> ID // AssociateFailures/DisassociateFailures let tests force a failure for // a specific floating-IP ID on its next call, to exercise retry paths. AssociateFailures map[string]error DisassociateFailures map[string]error } func NewMockClient() *MockClient { return &MockClient{ fips: make(map[string]*FloatingIP), byIP: make(map[string]string), } } // Seed registers a floating IP as if pre-allocated in the service project. func (m *MockClient) Seed(id, address, projectID string) { m.mu.Lock() defer m.mu.Unlock() m.fips[id] = &FloatingIP{ID: id, Address: address, ProjectID: projectID} m.byIP[address] = id } // SeedWithPort registers a floating IP as already associated to portID — // for simulating a FIP that's occupied (e.g. by another VM's port) before a // test's Tick runs. func (m *MockClient) SeedWithPort(id, address, projectID, portID string) { m.mu.Lock() defer m.mu.Unlock() m.fips[id] = &FloatingIP{ID: id, Address: address, ProjectID: projectID, PortID: portID} m.byIP[address] = id } func (m *MockClient) GetFloatingIPByAddress(ctx context.Context, address string) (*FloatingIP, error) { m.mu.Lock() defer m.mu.Unlock() id, ok := m.byIP[address] if !ok { return nil, ErrNotFound(address) } f := *m.fips[id] return &f, nil } func (m *MockClient) AssociateFloatingIP(ctx context.Context, fipID, portID string) error { m.mu.Lock() defer m.mu.Unlock() if err := m.AssociateFailures[fipID]; err != nil { delete(m.AssociateFailures, fipID) return err } f, ok := m.fips[fipID] if !ok { return fmt.Errorf("mock openstack: unknown floating ip %q", fipID) } f.PortID = portID return nil } func (m *MockClient) DisassociateFloatingIP(ctx context.Context, fipID string) error { m.mu.Lock() defer m.mu.Unlock() if err := m.DisassociateFailures[fipID]; err != nil { delete(m.DisassociateFailures, fipID) return err } f, ok := m.fips[fipID] if !ok { return fmt.Errorf("mock openstack: unknown floating ip %q", fipID) } f.PortID = "" return nil }