Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 203 additions & 1 deletion internal/controllers/user/actuator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ limitations under the License.
package user

import (
"context"
"testing"

"github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users"
orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
"go.uber.org/mock/gomock"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
"github.com/k-orc/openstack-resource-controller/v2/internal/osclients/mock"
)

func TestNeedsUpdate(t *testing.T) {
Expand Down Expand Up @@ -117,3 +126,196 @@ func TestHandleDescriptionUpdate(t *testing.T) {

}
}

func TestHandleEnabledUpdate(t *testing.T) {
ptrToBool := ptr.To[bool]
testCases := []struct {
name string
newValue *bool
existingValue bool
expectChange bool
}{
{name: "Identical", newValue: ptrToBool(true), existingValue: true, expectChange: false},
{name: "Different", newValue: ptrToBool(true), existingValue: false, expectChange: true},
{name: "No value provided, existing is set", newValue: nil, existingValue: false, expectChange: true},
{name: "No value provided, existing is default", newValue: nil, existingValue: true, expectChange: false},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
resource := &orcv1alpha1.UserResourceSpec{Enabled: tt.newValue}
osResource := &users.User{Enabled: tt.existingValue}

updateOpts := users.UpdateOpts{}
handleEnabledUpdate(&updateOpts, resource, osResource)

got, _ := needsUpdate(updateOpts)
if got != tt.expectChange {
t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
}
})
}
}

func TestReconcilePassword(t *testing.T) {
ptrToPasswordRef := ptr.To[orcv1alpha1.KubernetesNameRef]
testCases := []struct {
name string
orcObject *orcv1alpha1.User
osResource *users.User
secret *corev1.Secret
setupMock func(*mock.MockUserClientMockRecorder)
wantReschedule bool
wantErr bool
}{
{
name: "No password ref set",
orcObject: &orcv1alpha1.User{
Spec: orcv1alpha1.UserSpec{
Resource: &orcv1alpha1.UserResourceSpec{},
},
},
osResource: &users.User{ID: "user-id"},
wantReschedule: false,
wantErr: false,
},
{
name: "Resource is nil",
orcObject: &orcv1alpha1.User{
Spec: orcv1alpha1.UserSpec{},
},
osResource: &users.User{ID: "user-id"},
wantReschedule: false,
wantErr: false,
},
{
name: "Password ref unchanged",
orcObject: &orcv1alpha1.User{
Spec: orcv1alpha1.UserSpec{
Resource: &orcv1alpha1.UserResourceSpec{
PasswordRef: ptrToPasswordRef("my-secret"),
},
},
Status: orcv1alpha1.UserStatus{
Resource: &orcv1alpha1.UserResourceStatus{
AppliedPasswordRef: "my-secret",
},
},
},
osResource: &users.User{ID: "user-id"},
wantReschedule: false,
wantErr: false,
},
{
name: "First password set - no UpdateUser call",
orcObject: &orcv1alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "test-user",
Namespace: "test-ns",
UID: "test-uid",
},
Spec: orcv1alpha1.UserSpec{
Resource: &orcv1alpha1.UserResourceSpec{
PasswordRef: ptrToPasswordRef("my-secret"),
},
},
Status: orcv1alpha1.UserStatus{
Resource: &orcv1alpha1.UserResourceStatus{
AppliedPasswordRef: "",
},
},
},
osResource: &users.User{ID: "user-id"},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"password": []byte("mypassword123"),
},
},
// No UpdateUser call expected on first reconcile
setupMock: func(recorder *mock.MockUserClientMockRecorder) {},
wantReschedule: false,
wantErr: false,
},
{
name: "Password changed - UpdateUser called",
orcObject: &orcv1alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "test-user",
Namespace: "test-ns",
UID: "test-uid",
},
Spec: orcv1alpha1.UserSpec{
Resource: &orcv1alpha1.UserResourceSpec{
PasswordRef: ptrToPasswordRef("my-secret"),
},
},
Status: orcv1alpha1.UserStatus{
Resource: &orcv1alpha1.UserResourceStatus{
AppliedPasswordRef: "old-secret",
},
},
},
osResource: &users.User{ID: "user-id"},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"password": []byte("newpassword456"),
},
},
setupMock: func(recorder *mock.MockUserClientMockRecorder) {
recorder.UpdateUser(gomock.Any(), "user-id", gomock.Any()).Return(&users.User{}, nil)
},
wantReschedule: true, // NeedsRefresh returns true
wantErr: false,
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
mockctrl := gomock.NewController(t)
userClient := mock.NewMockUserClient(mockctrl)

// Create fake k8s client
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = orcv1alpha1.AddToScheme(scheme)

objects := []client.Object{tt.orcObject}
if tt.secret != nil {
objects = append(objects, tt.secret)
}

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objects...).
WithStatusSubresource(&orcv1alpha1.User{}).
Build()

actuator := userActuator{
osClient: userClient,
k8sClient: k8sClient,
}

if tt.setupMock != nil {
tt.setupMock(userClient.EXPECT())
}

reconcileStatus := actuator.reconcilePassword(context.TODO(), tt.orcObject, tt.osResource)

needsReschedule, err := reconcileStatus.NeedsReschedule()
if (err != nil) != tt.wantErr {
t.Errorf("reconcilePassword() error = %v, wantErr %v", err, tt.wantErr)
}
if needsReschedule != tt.wantReschedule {
t.Errorf("reconcilePassword() needsReschedule = %v, want %v", needsReschedule, tt.wantReschedule)
}
})
}
}
Loading