Skip to content

Commit

Permalink
✅ ♻️ refactor and cover getRawUserPassword with tests (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
DrPsychick authored Dec 21, 2024
1 parent 4cd47de commit 7702a73
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 16 deletions.
57 changes: 41 additions & 16 deletions internal/controller/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controller

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -251,21 +252,11 @@ func (r *UserReconciler) createUser(ctx context.Context, user *operatorv1alpha1.
// raw password is required during creation
if user.Spec.RawPassword == "" {
var err error
if user.Spec.PasswordSecret != "" && user.Spec.PasswordKey != "" {
user.Spec.RawPassword, err = r.getUserPassword(ctx, user.Namespace, user.Spec.PasswordSecret, user.Spec.PasswordKey)
if err != nil {
logr.Error(err, fmt.Sprintf("failed to get password from secret %s/%s", user.Namespace, user.Spec.PasswordSecret))
return true, err
}
logr.Info(fmt.Sprintf("using password from secret for user %s", email))
} else {
// initial random password if none given
user.Spec.RawPassword, err = password.Generate(20, 2, 2, false, false)
if err != nil {
logr.Error(err, fmt.Sprintf("failed to generate password for user %s", email))
return true, err
}
logr.Info(fmt.Sprintf("using generated password for user %s", email))
user.Spec.RawPassword, err = r.getRawUserPassword(ctx, user)
if err != nil {
logr.Error(err, fmt.Sprintf("failed to get password for user %s", email))
// retry, because Secret could appear
return true, err
}
}

Expand Down Expand Up @@ -410,13 +401,47 @@ func (r *UserReconciler) userFromSpec(spec operatorv1alpha1.UserSpec) (mailu.Use
return u, nil
}

func (r *UserReconciler) getRawUserPassword(ctx context.Context, user *operatorv1alpha1.User) (string, error) {
var err error
pass := ""
email := user.Spec.Name + "@" + user.Spec.Domain
if user.Spec.PasswordSecret != "" && user.Spec.PasswordKey != "" {
pass, err = r.getUserPassword(ctx, user.Namespace, user.Spec.PasswordSecret, user.Spec.PasswordKey)
if err != nil {
log.FromContext(ctx).Error(err, fmt.Sprintf("failed to get password from secret %s/%s", user.Namespace, user.Spec.PasswordSecret))
return pass, err
}
log.FromContext(ctx).Info(fmt.Sprintf("using password from secret for user %s", email))
} else {
// initial random password if none given
pass, err = password.Generate(20, 2, 2, false, false)
if err != nil {
log.FromContext(ctx).Error(err, fmt.Sprintf("failed to generate password for user %s", email))
return pass, err
}
log.FromContext(ctx).Info(fmt.Sprintf("using generated password for user %s", email))
}
return pass, nil
}

func (r *UserReconciler) getUserPassword(ctx context.Context, namespace, secret, key string) (string, error) {
s := &corev1.Secret{}
err := r.Get(ctx, types.NamespacedName{Name: secret, Namespace: namespace}, s)
if err != nil {
return "", err
}
return string(s.Data[key]), nil

if _, ok := s.Data[key]; !ok {
return "", errors.New("secret does not contain key " + key)
}

pass := make([]byte, base64.StdEncoding.DecodedLen(len(s.Data[key])))
decoded, err := base64.StdEncoding.Decode(pass, s.Data[key])
if err != nil {
return "", err
}

return string(pass[:decoded]), nil
}

func getUserReadyCondition(status metav1.ConditionStatus, reason, message string) metav1.Condition {
Expand Down
174 changes: 174 additions & 0 deletions internal/controller/user_controller_private_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package controller

import (
"context"
"encoding/base64"
"regexp"
"testing"

operatorv1alpha1 "github.com/sickhub/mailu-operator/api/v1alpha1"
"github.com/sickhub/mailu-operator/pkg/mailu"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestUserReconciler_getRawUserPassword(t *testing.T) {
type fields struct {
Client client.Client
Scheme *runtime.Scheme
ApiURL string
ApiToken string
ApiClient *mailu.Client
}
type args struct {
ctx context.Context
user *operatorv1alpha1.User
}

k8sClient := fake.NewClientBuilder().Build()
k8sClient.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}})
passworB64 := make([]byte, base64.StdEncoding.EncodedLen(8))
base64.StdEncoding.Encode(passworB64, []byte("password"))
k8sClient.Create(context.Background(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
},
Data: map[string][]byte{
"password": passworB64,
},
})

tests := []struct {
name string
fields fields
args args
wantRegex string
wantErr bool
}{
{
name: "test generated password",
fields: fields{
Client: nil,
Scheme: nil,
ApiURL: "",
ApiToken: "",
ApiClient: nil,
},
args: args{
ctx: context.Background(),
user: &operatorv1alpha1.User{
Spec: operatorv1alpha1.UserSpec{
Name: "test",
Domain: "test.com",
},
},
},
wantRegex: ".{20}",
wantErr: false,
},
{
name: "test missing secret",
fields: fields{
Client: k8sClient,
Scheme: nil,
ApiURL: "",
ApiToken: "",
ApiClient: nil,
},
args: args{
ctx: context.Background(),
user: &operatorv1alpha1.User{
Spec: operatorv1alpha1.UserSpec{
Name: "test",
Domain: "test.com",
PasswordSecret: "foo",
PasswordKey: "bar",
},
},
},
wantRegex: "",
wantErr: true,
},
{
name: "test existing secret",
fields: fields{
Client: k8sClient,
Scheme: nil,
ApiURL: "",
ApiToken: "",
ApiClient: nil,
},
args: args{
ctx: context.Background(),
user: &operatorv1alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "testUser",
Namespace: "default",
},
Spec: operatorv1alpha1.UserSpec{
Name: "test",
Domain: "test.com",
PasswordSecret: "secret",
PasswordKey: "password",
},
},
},
wantRegex: "(password)",
wantErr: false,
},
{
name: "test existing secret with wrong key",
fields: fields{
Client: k8sClient,
Scheme: nil,
ApiURL: "",
ApiToken: "",
ApiClient: nil,
},
args: args{
ctx: context.Background(),
user: &operatorv1alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "testUser",
Namespace: "default",
},
Spec: operatorv1alpha1.UserSpec{
Name: "test",
Domain: "test.com",
PasswordSecret: "secret",
PasswordKey: "non-existent",
},
},
},
wantRegex: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &UserReconciler{
Client: tt.fields.Client,
Scheme: tt.fields.Scheme,
ApiURL: tt.fields.ApiURL,
ApiToken: tt.fields.ApiToken,
ApiClient: tt.fields.ApiClient,
}
got, err := r.getRawUserPassword(tt.args.ctx, tt.args.user)
if (err != nil) != tt.wantErr {
t.Errorf("getRawUserPassword() error = %v, wantErr %v", err, tt.wantErr)
return
}
reg, err := regexp.Compile(tt.wantRegex)
if err != nil {
t.Fatal(err)
}
if !reg.MatchString(got) {
t.Errorf("getRawUserPassword() %v to match regex %v", got, tt.wantRegex)
}
})
}
}

0 comments on commit 7702a73

Please sign in to comment.