Repository: spidernet-io/spiderpool
Pull request #1740 (2023-04-28 06:47:15)

add ippool-informer unit test
Signed-off-by: Icarus9913 [icaruswu66@qq.com](mailto:icaruswu66@qq.com)

sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=spidernet-io_spiderpool&pullRequest=1740)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=SECURITY_HOTSPOT) [![E](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/E-16px.png 'E')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=SECURITY_HOTSPOT) [6 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=1740&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=1740) No Coverage information  
[![5.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '5.0%')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=1740&metric=new_duplicated_lines_density&view=list) [5.0% Duplication](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=1740&metric=new_duplicated_lines_density&view=list)


codecov[bot]: ## [Codecov](https://codecov.io/gh/spidernet-io/spiderpool/pull/1740?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) Report
> Merging [#1740](https://codecov.io/gh/spidernet-io/spiderpool/pull/1740?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) (dac163a) into [main](https://codecov.io/gh/spidernet-io/spiderpool/commit/e6a627d7371e36335ce569de60428fdf1f75265f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) (e6a627d) will **increase** coverage by `2.21%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/spidernet-io/spiderpool/pull/1740/graphs/tree.svg?width=650&height=150&src=pr&token=YKXY2E4Q8G&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io)](https://codecov.io/gh/spidernet-io/spiderpool/pull/1740?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io)

```diff
@@            Coverage Diff             @@
##             main    #1740      +/-   ##
==========================================
+ Coverage   76.19%   78.40%   +2.21%     
==========================================
  Files          49       49              
  Lines        5108     5108              
==========================================
+ Hits         3892     4005     +113     
+ Misses       1097      964     -133     
- Partials      119      139      +20     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `78.40% <ø> (+2.21%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io#carryforward-flags-in-the-pull-request-comment) to find out more.

[see 1 file with indirect coverage changes](https://codecov.io/gh/spidernet-io/spiderpool/pull/1740/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io)


Patch:
diff --git a/vendor/k8s.io/client-go/dynamic/fake/simple.go b/vendor/k8s.io/client-go/dynamic/fake/simple.go
new file mode 100644
index 000000000..5d0a6f69f
--- /dev/null
+++ b/vendor/k8s.io/client-go/dynamic/fake/simple.go
@@ -0,0 +1,539 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/testing"
+)
+
+func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeDynamicClient {
+	unstructuredScheme := runtime.NewScheme()
+	for gvk := range scheme.AllKnownTypes() {
+		if unstructuredScheme.Recognizes(gvk) {
+			continue
+		}
+		if strings.HasSuffix(gvk.Kind, "List") {
+			unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.UnstructuredList{})
+			continue
+		}
+		unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.Unstructured{})
+	}
+
+	objects, err := convertObjectsToUnstructured(scheme, objects)
+	if err != nil {
+		panic(err)
+	}
+
+	for _, obj := range objects {
+		gvk := obj.GetObjectKind().GroupVersionKind()
+		if !unstructuredScheme.Recognizes(gvk) {
+			unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.Unstructured{})
+		}
+		gvk.Kind += "List"
+		if !unstructuredScheme.Recognizes(gvk) {
+			unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.UnstructuredList{})
+		}
+	}
+
+	return NewSimpleDynamicClientWithCustomListKinds(unstructuredScheme, nil, objects...)
+}
+
+// NewSimpleDynamicClientWithCustomListKinds try not to use this.  In general you want to have the scheme have the List types registered
+// and allow the default guessing for resources match.  Sometimes that doesn't work, so you can specify a custom mapping here.
+func NewSimpleDynamicClientWithCustomListKinds(scheme *runtime.Scheme, gvrToListKind map[schema.GroupVersionResource]string, objects ...runtime.Object) *FakeDynamicClient {
+	// In order to use List with this client, you have to have your lists registered so that the object tracker will find them
+	// in the scheme to support the t.scheme.New(listGVK) call when it's building the return value.
+	// Since the base fake client needs the listGVK passed through the action (in cases where there are no instances, it
+	// cannot look up the actual hits), we need to know a mapping of GVR to listGVK here.  For GETs and other types of calls,
+	// there is no return value that contains a GVK, so it doesn't have to know the mapping in advance.
+
+	// first we attempt to invert known List types from the scheme to auto guess the resource with unsafe guesses
+	// this covers common usage of registering types in scheme and passing them
+	completeGVRToListKind := map[schema.GroupVersionResource]string{}
+	for listGVK := range scheme.AllKnownTypes() {
+		if !strings.HasSuffix(listGVK.Kind, "List") {
+			continue
+		}
+		nonListGVK := listGVK.GroupVersion().WithKind(listGVK.Kind[:len(listGVK.Kind)-4])
+		plural, _ := meta.UnsafeGuessKindToResource(nonListGVK)
+		completeGVRToListKind[plural] = listGVK.Kind
+	}
+
+	for gvr, listKind := range gvrToListKind {
+		if !strings.HasSuffix(listKind, "List") {
+			panic("coding error, listGVK must end in List or this fake client doesn't work right")
+		}
+		listGVK := gvr.GroupVersion().WithKind(listKind)
+
+		// if we already have this type registered, just skip it
+		if _, err := scheme.New(listGVK); err == nil {
+			completeGVRToListKind[gvr] = listKind
+			continue
+		}
+
+		scheme.AddKnownTypeWithName(listGVK, &unstructured.UnstructuredList{})
+		completeGVRToListKind[gvr] = listKind
+	}
+
+	codecs := serializer.NewCodecFactory(scheme)
+	o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
+	for _, obj := range objects {
+		if err := o.Add(obj); err != nil {
+			panic(err)
+		}
+	}
+
+	cs := &FakeDynamicClient{scheme: scheme, gvrToListKind: completeGVRToListKind, tracker: o}
+	cs.AddReactor("*", "*", testing.ObjectReaction(o))
+	cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
+		gvr := action.GetResource()
+		ns := action.GetNamespace()
+		watch, err := o.Watch(gvr, ns)
+		if err != nil {
+			return false, nil, err
+		}
+		return true, watch, nil
+	})
+
+	return cs
+}
+
+// Clientset implements clientset.Interface. Meant to be embedded into a
+// struct to get a default implementation. This makes faking out just the method
+// you want to test easier.
+type FakeDynamicClient struct {
+	testing.Fake
+	scheme        *runtime.Scheme
+	gvrToListKind map[schema.GroupVersionResource]string
+	tracker       testing.ObjectTracker
+}
+
+type dynamicResourceClient struct {
+	client    *FakeDynamicClient
+	namespace string
+	resource  schema.GroupVersionResource
+	listKind  string
+}
+
+var (
+	_ dynamic.Interface  = &FakeDynamicClient{}
+	_ testing.FakeClient = &FakeDynamicClient{}
+)
+
+func (c *FakeDynamicClient) Tracker() testing.ObjectTracker {
+	return c.tracker
+}
+
+func (c *FakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
+	return &dynamicResourceClient{client: c, resource: resource, listKind: c.gvrToListKind[resource]}
+}
+
+func (c *dynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface {
+	ret := *c
+	ret.namespace = ns
+	return &ret
+}
+
+func (c *dynamicResourceClient) Create(ctx context.Context, obj *unstructured.Unstructured, opts metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	var uncastRet runtime.Object
+	var err error
+	switch {
+	case len(c.namespace) == 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootCreateAction(c.resource, obj), obj)
+
+	case len(c.namespace) == 0 && len(subresources) > 0:
+		var accessor metav1.Object // avoid shadowing err
+		accessor, err = meta.Accessor(obj)
+		if err != nil {
+			return nil, err
+		}
+		name := accessor.GetName()
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootCreateSubresourceAction(c.resource, name, strings.Join(subresources, "/"), obj), obj)
+
+	case len(c.namespace) > 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewCreateAction(c.resource, c.namespace, obj), obj)
+
+	case len(c.namespace) > 0 && len(subresources) > 0:
+		var accessor metav1.Object // avoid shadowing err
+		accessor, err = meta.Accessor(obj)
+		if err != nil {
+			return nil, err
+		}
+		name := accessor.GetName()
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewCreateSubresourceAction(c.resource, name, strings.Join(subresources, "/"), c.namespace, obj), obj)
+
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if uncastRet == nil {
+		return nil, err
+	}
+
+	ret := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil {
+		return nil, err
+	}
+	return ret, err
+}
+
+func (c *dynamicResourceClient) Update(ctx context.Context, obj *unstructured.Unstructured, opts metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	var uncastRet runtime.Object
+	var err error
+	switch {
+	case len(c.namespace) == 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootUpdateAction(c.resource, obj), obj)
+
+	case len(c.namespace) == 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootUpdateSubresourceAction(c.resource, strings.Join(subresources, "/"), obj), obj)
+
+	case len(c.namespace) > 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewUpdateAction(c.resource, c.namespace, obj), obj)
+
+	case len(c.namespace) > 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewUpdateSubresourceAction(c.resource, strings.Join(subresources, "/"), c.namespace, obj), obj)
+
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if uncastRet == nil {
+		return nil, err
+	}
+
+	ret := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil {
+		return nil, err
+	}
+	return ret, err
+}
+
+func (c *dynamicResourceClient) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, error) {
+	var uncastRet runtime.Object
+	var err error
+	switch {
+	case len(c.namespace) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootUpdateSubresourceAction(c.resource, "status", obj), obj)
+
+	case len(c.namespace) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewUpdateSubresourceAction(c.resource, "status", c.namespace, obj), obj)
+
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if uncastRet == nil {
+		return nil, err
+	}
+
+	ret := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil {
+		return nil, err
+	}
+	return ret, err
+}
+
+func (c *dynamicResourceClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions, subresources ...string) error {
+	var err error
+	switch {
+	case len(c.namespace) == 0 && len(subresources) == 0:
+		_, err = c.client.Fake.
+			Invokes(testing.NewRootDeleteAction(c.resource, name), &metav1.Status{Status: "dynamic delete fail"})
+
+	case len(c.namespace) == 0 && len(subresources) > 0:
+		_, err = c.client.Fake.
+			Invokes(testing.NewRootDeleteSubresourceAction(c.resource, strings.Join(subresources, "/"), name), &metav1.Status{Status: "dynamic delete fail"})
+
+	case len(c.namespace) > 0 && len(subresources) == 0:
+		_, err = c.client.Fake.
+			Invokes(testing.NewDeleteAction(c.resource, c.namespace, name), &metav1.Status{Status: "dynamic delete fail"})
+
+	case len(c.namespace) > 0 && len(subresources) > 0:
+		_, err = c.client.Fake.
+			Invokes(testing.NewDeleteSubresourceAction(c.resource, strings.Join(subresources, "/"), c.namespace, name), &metav1.Status{Status: "dynamic delete fail"})
+	}
+
+	return err
+}
+
+func (c *dynamicResourceClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOptions metav1.ListOptions) error {
+	var err error
+	switch {
+	case len(c.namespace) == 0:
+		action := testing.NewRootDeleteCollectionAction(c.resource, listOptions)
+		_, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "dynamic deletecollection fail"})
+
+	case len(c.namespace) > 0:
+		action := testing.NewDeleteCollectionAction(c.resource, c.namespace, listOptions)
+		_, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "dynamic deletecollection fail"})
+
+	}
+
+	return err
+}
+
+func (c *dynamicResourceClient) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	var uncastRet runtime.Object
+	var err error
+	switch {
+	case len(c.namespace) == 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootGetAction(c.resource, name), &metav1.Status{Status: "dynamic get fail"})
+
+	case len(c.namespace) == 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootGetSubresourceAction(c.resource, strings.Join(subresources, "/"), name), &metav1.Status{Status: "dynamic get fail"})
+
+	case len(c.namespace) > 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewGetAction(c.resource, c.namespace, name), &metav1.Status{Status: "dynamic get fail"})
+
+	case len(c.namespace) > 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewGetSubresourceAction(c.resource, c.namespace, strings.Join(subresources, "/"), name), &metav1.Status{Status: "dynamic get fail"})
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if uncastRet == nil {
+		return nil, err
+	}
+
+	ret := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil {
+		return nil, err
+	}
+	return ret, err
+}
+
+func (c *dynamicResourceClient) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
+	if len(c.listKind) == 0 {
+		panic(fmt.Sprintf("coding error: you must register resource to list kind for every resource you're going to LIST when creating the client.  See NewSimpleDynamicClientWithCustomListKinds or register the list into the scheme: %v out of %v", c.resource, c.client.gvrToListKind))
+	}
+	listGVK := c.resource.GroupVersion().WithKind(c.listKind)
+	listForFakeClientGVK := c.resource.GroupVersion().WithKind(c.listKind[:len(c.listKind)-4]) /*base library appends List*/
+
+	var obj runtime.Object
+	var err error
+	switch {
+	case len(c.namespace) == 0:
+		obj, err = c.client.Fake.
+			Invokes(testing.NewRootListAction(c.resource, listForFakeClientGVK, opts), &metav1.Status{Status: "dynamic list fail"})
+
+	case len(c.namespace) > 0:
+		obj, err = c.client.Fake.
+			Invokes(testing.NewListAction(c.resource, listForFakeClientGVK, c.namespace, opts), &metav1.Status{Status: "dynamic list fail"})
+
+	}
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+
+	retUnstructured := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(obj, retUnstructured, nil); err != nil {
+		return nil, err
+	}
+	entireList, err := retUnstructured.ToList()
+	if err != nil {
+		return nil, err
+	}
+
+	list := &unstructured.UnstructuredList{}
+	list.SetRemainingItemCount(entireList.GetRemainingItemCount())
+	list.SetResourceVersion(entireList.GetResourceVersion())
+	list.SetContinue(entireList.GetContinue())
+	list.GetObjectKind().SetGroupVersionKind(listGVK)
+	for i := range entireList.Items {
+		item := &entireList.Items[i]
+		metadata, err := meta.Accessor(item)
+		if err != nil {
+			return nil, err
+		}
+		if label.Matches(labels.Set(metadata.GetLabels())) {
+			list.Items = append(list.Items, *item)
+		}
+	}
+	return list, nil
+}
+
+func (c *dynamicResourceClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
+	switch {
+	case len(c.namespace) == 0:
+		return c.client.Fake.
+			InvokesWatch(testing.NewRootWatchAction(c.resource, opts))
+
+	case len(c.namespace) > 0:
+		return c.client.Fake.
+			InvokesWatch(testing.NewWatchAction(c.resource, c.namespace, opts))
+
+	}
+
+	panic("math broke")
+}
+
+// TODO: opts are currently ignored.
+func (c *dynamicResourceClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	var uncastRet runtime.Object
+	var err error
+	switch {
+	case len(c.namespace) == 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootPatchAction(c.resource, name, pt, data), &metav1.Status{Status: "dynamic patch fail"})
+
+	case len(c.namespace) == 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootPatchSubresourceAction(c.resource, name, pt, data, subresources...), &metav1.Status{Status: "dynamic patch fail"})
+
+	case len(c.namespace) > 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewPatchAction(c.resource, c.namespace, name, pt, data), &metav1.Status{Status: "dynamic patch fail"})
+
+	case len(c.namespace) > 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewPatchSubresourceAction(c.resource, c.namespace, name, pt, data, subresources...), &metav1.Status{Status: "dynamic patch fail"})
+
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if uncastRet == nil {
+		return nil, err
+	}
+
+	ret := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil {
+		return nil, err
+	}
+	return ret, err
+}
+
+// TODO: opts are currently ignored.
+func (c *dynamicResourceClient) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	outBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
+	if err != nil {
+		return nil, err
+	}
+	var uncastRet runtime.Object
+	switch {
+	case len(c.namespace) == 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootPatchAction(c.resource, name, types.ApplyPatchType, outBytes), &metav1.Status{Status: "dynamic patch fail"})
+
+	case len(c.namespace) == 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewRootPatchSubresourceAction(c.resource, name, types.ApplyPatchType, outBytes, subresources...), &metav1.Status{Status: "dynamic patch fail"})
+
+	case len(c.namespace) > 0 && len(subresources) == 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewPatchAction(c.resource, c.namespace, name, types.ApplyPatchType, outBytes), &metav1.Status{Status: "dynamic patch fail"})
+
+	case len(c.namespace) > 0 && len(subresources) > 0:
+		uncastRet, err = c.client.Fake.
+			Invokes(testing.NewPatchSubresourceAction(c.resource, c.namespace, name, types.ApplyPatchType, outBytes, subresources...), &metav1.Status{Status: "dynamic patch fail"})
+
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if uncastRet == nil {
+		return nil, err
+	}
+
+	ret := &unstructured.Unstructured{}
+	if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil {
+		return nil, err
+	}
+	return ret, nil
+}
+
+func (c *dynamicResourceClient) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) {
+	return c.Apply(ctx, name, obj, options, "status")
+}
+
+func convertObjectsToUnstructured(s *runtime.Scheme, objs []runtime.Object) ([]runtime.Object, error) {
+	ul := make([]runtime.Object, 0, len(objs))
+
+	for _, obj := range objs {
+		u, err := convertToUnstructured(s, obj)
+		if err != nil {
+			return nil, err
+		}
+
+		ul = append(ul, u)
+	}
+	return ul, nil
+}
+
+func convertToUnstructured(s *runtime.Scheme, obj runtime.Object) (runtime.Object, error) {
+	var (
+		err error
+		u   unstructured.Unstructured
+	)
+
+	u.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+	if err != nil {
+		return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
+	}
+
+	gvk := u.GroupVersionKind()
+	if gvk.Group == "" || gvk.Kind == "" {
+		gvks, _, err := s.ObjectKinds(obj)
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert to unstructured - unable to get GVK %w", err)
+		}
+		apiv, k := gvks[0].ToAPIVersionAndKind()
+		u.SetAPIVersion(apiv)
+		u.SetKind(k)
+	}
+	return &u, nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 166b999d6..f5cda1e3b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -787,6 +787,7 @@ k8s.io/client-go/discovery/fake
 k8s.io/client-go/dynamic
 k8s.io/client-go/dynamic/dynamicinformer
 k8s.io/client-go/dynamic/dynamiclister
+k8s.io/client-go/dynamic/fake
 k8s.io/client-go/informers
 k8s.io/client-go/informers/admissionregistration
 k8s.io/client-go/informers/admissionregistration/v1


Test patch:
diff --git a/pkg/ippoolmanager/ippool_informer_test.go b/pkg/ippoolmanager/ippool_informer_test.go
new file mode 100644
index 000000000..bb567cd08
--- /dev/null
+++ b/pkg/ippoolmanager/ippool_informer_test.go
@@ -0,0 +1,236 @@
+// Copyright 2022 Authors of spidernet-io
+// SPDX-License-Identifier: Apache-2.0
+
+package ippoolmanager
+
+import (
+	"context"
+	"fmt"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/agiledragon/gomonkey/v2"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	dynamicfake "k8s.io/client-go/dynamic/fake"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/spidernet-io/spiderpool/pkg/applicationcontroller/applicationinformers"
+	"github.com/spidernet-io/spiderpool/pkg/constant"
+	spiderpoolv2beta1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/spiderpool.spidernet.io/v2beta1"
+	spiderpoolfake "github.com/spidernet-io/spiderpool/pkg/k8s/client/clientset/versioned/fake"
+	"github.com/spidernet-io/spiderpool/pkg/k8s/client/informers/externalversions"
+	"github.com/spidernet-io/spiderpool/pkg/metric"
+	"github.com/spidernet-io/spiderpool/pkg/types"
+)
+
+var _ = Describe("IPPool-informer", Label("unitest"), Ordered, func() {
+	BeforeAll(func() {
+		_, err := metric.InitMetric(context.TODO(), constant.SpiderpoolController, false, false)
+		Expect(err).NotTo(HaveOccurred())
+		err = metric.InitSpiderpoolControllerMetrics(context.TODO())
+		Expect(err).NotTo(HaveOccurred())
+	})
+
+	var pool *spiderpoolv2beta1.SpiderIPPool
+	BeforeEach(func() {
+		pool = &spiderpoolv2beta1.SpiderIPPool{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       constant.KindSpiderIPPool,
+				APIVersion: fmt.Sprintf("%s/%s", constant.SpiderpoolAPIGroup, constant.SpiderpoolAPIVersion),
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:   "test-ippool",
+				Labels: map[string]string{},
+			},
+			Spec: spiderpoolv2beta1.IPPoolSpec{
+				IPVersion: pointer.Int64(4),
+				Subnet:    "10.1.0.0/16",
+				IPs:       []string{"10.1.0.1-10.1.0.10"},
+			},
+			Status: spiderpoolv2beta1.IPPoolStatus{
+				AllocatedIPs:     nil,
+				TotalIPCount:     pointer.Int64(10),
+				AllocatedIPCount: pointer.Int64(0),
+			},
+		}
+	})
+
+	Describe("run ippool controller", func() {
+		var control *poolController
+		BeforeEach(func() {
+			scheme = runtime.NewScheme()
+			err := spiderpoolv2beta1.AddToScheme(scheme)
+			Expect(err).NotTo(HaveOccurred())
+			err = appsv1.AddToScheme(scheme)
+			Expect(err).NotTo(HaveOccurred())
+
+			control = newController()
+			fakeClientSet := spiderpoolfake.NewSimpleClientset()
+			factory := externalversions.NewSharedInformerFactory(fakeClientSet, 0)
+			err = control.addEventHandlers(factory.Spiderpool().V2beta1().SpiderIPPools())
+			Expect(err).NotTo(HaveOccurred())
+			control.ipPoolStore = factory.Spiderpool().V2beta1().SpiderIPPools().Informer().GetStore()
+		})
+
+		Context("enqueue an IPPool", func() {
+			It("enqueue an normal IPPool that informer synced", func() {
+				ctx, cancel := context.WithCancel(context.TODO())
+				defer cancel()
+
+				pool.Status = spiderpoolv2beta1.IPPoolStatus{}
+
+				go func() {
+					defer GinkgoRecover()
+
+					patches := gomonkey.ApplyFuncReturn(cache.WaitForCacheSync, true)
+					defer patches.Reset()
+
+					err := control.Run(ctx.Done())
+					if nil != err {
+						cancel()
+						Fail(err.Error())
+					}
+				}()
+
+				time.Sleep(time.Second)
+				err := control.ipPoolStore.Add(pool)
+				Expect(err).NotTo(HaveOccurred())
+				control.enqueueIPPool(pool)
+			})
+
+			It("enqueue an normal IPPool that informer didn't synced", func() {
+				ctx, cancel := context.WithCancel(context.TODO())
+				defer cancel()
+
+				go func() {
+					defer GinkgoRecover()
+
+					patches := gomonkey.ApplyFuncReturn(cache.WaitForCacheSync, true)
+					defer patches.Reset()
+
+					err := control.Run(ctx.Done())
+					if nil != err {
+						cancel()
+						Fail(err.Error())
+					}
+				}()
+
+				time.Sleep(time.Second)
+				control.enqueueIPPool(pool)
+			})
+
+			It("enqueue an auto-created IPPool that informer synced", func() {
+				tmpPool := pool.DeepCopy()
+				tmpPool.Name = "auto4-deploy-abc"
+				labels := map[string]string{
+					constant.LabelIPPoolOwnerApplication: applicationinformers.ApplicationNamespacedName(types.AppNamespacedName{
+						APIVersion: appsv1.SchemeGroupVersion.String(),
+						Kind:       constant.KindDeployment,
+						Namespace:  "test-ns",
+						Name:       "test-name",
+					}),
+					constant.LabelIPPoolReclaimIPPool: constant.True,
+				}
+				tmpPool.SetLabels(labels)
+
+				ctx, cancel := context.WithCancel(context.TODO())
+				defer cancel()
+
+				go func() {
+					defer GinkgoRecover()
+
+					patches := gomonkey.ApplyFuncReturn(cache.WaitForCacheSync, true)
+					defer patches.Reset()
+
+					err := control.Run(ctx.Done())
+					if nil != err {
+						cancel()
+						Fail(err.Error())
+					}
+				}()
+
+				time.Sleep(time.Second)
+				err := control.ipPoolStore.Add(tmpPool)
+				Expect(err).NotTo(HaveOccurred())
+				control.enqueueIPPool(tmpPool)
+			})
+
+			It("enqueue an terminating auto-created IPPool that informer synced", func() {
+				tmpPool := pool.DeepCopy()
+				tmpPool.Name = "auto4-deploy-abc"
+				now := metav1.Now()
+				tmpPool.DeletionTimestamp = now.DeepCopy()
+				labels := map[string]string{
+					constant.LabelIPPoolOwnerApplication: applicationinformers.ApplicationNamespacedName(types.AppNamespacedName{
+						APIVersion: appsv1.SchemeGroupVersion.String(),
+						Kind:       constant.KindDeployment,
+						Namespace:  "test-ns",
+						Name:       "test-name",
+					}),
+					constant.LabelIPPoolReclaimIPPool: constant.True,
+				}
+				tmpPool.SetLabels(labels)
+				controllerutil.AddFinalizer(tmpPool, constant.SpiderFinalizer)
+
+				ctx, cancel := context.WithCancel(context.TODO())
+				defer cancel()
+
+				go func() {
+					defer GinkgoRecover()
+
+					patches := gomonkey.ApplyFuncReturn(cache.WaitForCacheSync, true)
+					defer patches.Reset()
+
+					err := control.Run(ctx.Done())
+					if nil != err {
+						cancel()
+						Fail(err.Error())
+					}
+				}()
+
+				time.Sleep(time.Second)
+				err := control.ipPoolStore.Add(tmpPool)
+				Expect(err).NotTo(HaveOccurred())
+				control.enqueueIPPool(tmpPool)
+			})
+		})
+
+	})
+
+})
+
+var scheme *runtime.Scheme
+var poolControllerConfig IPPoolControllerConfig
+
+type poolController struct {
+	*IPPoolController
+	ipPoolStore cache.Store
+}
+
+func newController() *poolController {
+	fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+	fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
+
+	poolControllerConfig = IPPoolControllerConfig{
+		IPPoolControllerWorkers:       3,
+		EnableSpiderSubnet:            true,
+		MaxWorkqueueLength:            5000,
+		WorkQueueMaxRetries:           10,
+		LeaderRetryElectGap:           0,
+		WorkQueueRequeueDelayDuration: -1 * time.Second,
+		ResyncPeriod:                  10 * time.Second,
+	}
+
+	pController := NewIPPoolController(poolControllerConfig, fakeClient, fakeDynamicClient)
+
+	return &poolController{
+		IPPoolController: pController,
+	}
+}