Files
cron-checker/main_test.go
T

466 lines
12 KiB
Go

package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/sanity-io/litter"
cronjobv1 "k8s.io/api/batch/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
applyv1 "k8s.io/client-go/applyconfigurations/batch/v1"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
batchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
"k8s.io/client-go/rest"
)
func Test_Main(t *testing.T) {
tests := []struct {
name string
connectFunc func(provider ConfigProvider) (Client, error)
exitFunc func(code int)
}{
{
name: "error connecting to K8S",
connectFunc: func(ConfigProvider) (Client, error) {
return nil, errors.New("error")
},
exitFunc: func(code int) {
if code != 1 {
t.Errorf("main() got %d, want 1", code)
}
},
},
}
for _, tt := range tests {
os.Args = []string{"dummy", "--slack-url", "https://dummy.example.org"}
t.Run(tt.name, func(t *testing.T) {
exitFunc = tt.exitFunc
main()
})
}
}
func Test_doMain(t *testing.T) {
type args struct {
slackUrl string
provider ClientProvider
}
tests := []struct {
name string
args args
checkFunc func(client Client, slackUrl string, ic chan os.Signal, sleepTime time.Duration, out io.Writer) error
want int
}{
{
name: "error checking",
args: args{
provider: &brokenClientProvider{},
},
checkFunc: func(client Client, slackUrl string, ic chan os.Signal, sleepTime time.Duration, out io.Writer) error {
return errors.New("error")
},
want: 1,
},
{
name: "success",
args: args{
provider: &brokenClientProvider{},
},
checkFunc: func(client Client, slackUrl string, ic chan os.Signal, sleepTime time.Duration, out io.Writer) error {
return nil
},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkFunc = tt.checkFunc
if got := doMain(tt.args.slackUrl, tt.args.provider); got != tt.want {
t.Errorf("doMain() = %v, want %v", got, tt.want)
}
})
}
}
func Test_doCheck(t *testing.T) {
type args struct {
client Client
}
tests := []struct {
name string
args args
timeout time.Duration
slackResponse string
wantErr bool
wantOut []string
}{
{
name: "error getting cronjobs",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return nil, errors.New("error")
},
},
},
},
},
wantErr: true,
},
{
name: "no cronjobs",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{}, nil
},
},
},
},
},
timeout: time.Second,
wantErr: false,
},
{
name: "suspended cronjobs are ignored",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{
Items: []cronjobv1.CronJob{
{
Spec: cronjobv1.CronJobSpec{Suspend: boolP(true)},
},
},
}, nil
},
},
},
},
},
timeout: time.Second,
wantErr: false,
},
{
name: "invalid cron schedule",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{
Items: []cronjobv1.CronJob{
{
Spec: cronjobv1.CronJobSpec{Schedule: "abc"},
},
},
}, nil
},
},
},
},
},
wantErr: true,
},
{
name: "only correctly running cronjobs",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{
Items: []cronjobv1.CronJob{
{
ObjectMeta: v1.ObjectMeta{CreationTimestamp: v1.Time{Time: time.Now()}},
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *", Suspend: boolP(false)},
},
{
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now()}},
},
},
}, nil
},
},
},
},
},
timeout: time.Second,
wantErr: false,
},
{
name: "error in Slack call",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{
Items: []cronjobv1.CronJob{
{
ObjectMeta: v1.ObjectMeta{Name: "some-name", Namespace: "some-ns"},
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now().Add(-3 * time.Minute)}},
},
},
}, nil
},
},
},
},
},
timeout: time.Second,
slackResponse: "dummy",
wantErr: false,
wantOut: []string{"Checking some-ns/some-name since", "some-ns/some-name was not scheduled. Sending Slack notification.", "Unable to send Slack notification: slack: request failed statuscode: 200, message: invalid character 'd' looking for beginning of value"},
},
{
name: "Slack response not ok",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{
Items: []cronjobv1.CronJob{
{
ObjectMeta: v1.ObjectMeta{Name: "some-name", Namespace: "some-ns"},
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now().Add(-3 * time.Minute)}},
},
},
}, nil
},
},
},
},
},
timeout: time.Second,
slackResponse: `{"ok": false, "error": "Something went wrong"}`,
wantErr: false,
wantOut: []string{"Checking some-ns/some-name since", "some-ns/some-name was not scheduled. Sending Slack notification.", "Unable to send Slack notification: slack: request failed statuscode: 200, message: Something went wrong"},
},
{
name: "Slack response ok",
args: args{
client: &brokenClient{
batchApi: &batchApi{
cronApi: &cronApi{
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
return &cronjobv1.CronJobList{
Items: []cronjobv1.CronJob{
{
ObjectMeta: v1.ObjectMeta{Name: "some-name", Namespace: "some-ns"},
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now().Add(-3 * time.Minute)}},
},
},
}, nil
},
},
},
},
},
timeout: time.Second,
slackResponse: `{"ok": true}`,
wantErr: false,
wantOut: []string{"Checking some-ns/some-name since", "some-ns/some-name was not scheduled. Sending Slack notification."},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ic := make(chan os.Signal, 1)
if tt.timeout > 0 {
timeout := tt.timeout
fmt.Printf("Waiting %s before terminating\n", timeout.String())
go func() {
time.Sleep(timeout)
fmt.Println("Done waiting, terminating")
ic <- os.Interrupt
}()
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(tt.slackResponse))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
buff := &bytes.Buffer{}
if err := doCheck(tt.args.client, fmt.Sprintf("http://%s", baseURL), ic, 10*time.Millisecond, buff); (err != nil) != tt.wantErr {
t.Errorf("doCheck() error = %v, wantErr %v", err, tt.wantErr)
}
if len(tt.wantOut) > 0 {
for _, o := range tt.wantOut {
if !strings.Contains(buff.String(), o) {
t.Errorf("doCheck() got %s, want %s", buff.String(), o)
}
}
}
})
}
}
func TestDefaultProvider_Provide(t *testing.T) {
type fields struct {
provider ConfigProvider
}
tests := []struct {
name string
fields fields
want Client
wantErr bool
}{
{
name: "not in cluster",
fields: fields{provider: &InClusterProvider{}},
want: nil,
wantErr: true,
},
{
name: "dummy config",
fields: fields{provider: &dummyProvider{}},
want: &kubernetes.Clientset{
DiscoveryClient: &discovery.DiscoveryClient{
LegacyPrefix: "/api",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := DefaultProvider{
provider: tt.fields.provider,
}
got, err := d.Provide()
if (err != nil) != tt.wantErr {
t.Errorf("Provide() error = %v, wantErr %v", err, tt.wantErr)
return
}
gotDump := litter.Sdump(got)
wantDump := litter.Sdump(tt.want)
if gotDump != wantDump {
t.Errorf("Provide() got = %v, want %v", gotDump, wantDump)
}
})
}
}
type dummyProvider struct{}
func (d dummyProvider) Provide() (*rest.Config, error) {
return &rest.Config{}, nil
}
var _ ConfigProvider = &dummyProvider{}
type brokenClientProvider struct{}
func (b brokenClientProvider) Provide() (Client, error) {
return &brokenClient{}, nil
}
var _ ClientProvider = &brokenClientProvider{}
type brokenClient struct {
batchApi batchv1.BatchV1Interface
}
func (b brokenClient) BatchV1() batchv1.BatchV1Interface {
return b.batchApi
}
var _ Client = &brokenClient{}
type batchApi struct {
cronApi batchv1.CronJobInterface
}
func (b batchApi) RESTClient() rest.Interface {
panic("implement me")
}
func (b batchApi) CronJobs(namespace string) batchv1.CronJobInterface {
return b.cronApi
}
func (b batchApi) Jobs(namespace string) batchv1.JobInterface {
//TODO implement me
panic("implement me")
}
var _ batchv1.BatchV1Interface = &batchApi{}
type cronApi struct {
listFn func(ctx context.Context, opts v1.ListOptions) (*cronjobv1.CronJobList, error)
}
func (c cronApi) List(ctx context.Context, opts v1.ListOptions) (*cronjobv1.CronJobList, error) {
return c.listFn(ctx, opts)
}
func (c cronApi) Create(ctx context.Context, cronJob *cronjobv1.CronJob, opts v1.CreateOptions) (*cronjobv1.CronJob, error) {
panic("implement me")
}
func (c cronApi) Update(ctx context.Context, cronJob *cronjobv1.CronJob, opts v1.UpdateOptions) (*cronjobv1.CronJob, error) {
panic("implement me")
}
func (c cronApi) UpdateStatus(ctx context.Context, cronJob *cronjobv1.CronJob, opts v1.UpdateOptions) (*cronjobv1.CronJob, error) {
panic("implement me")
}
func (c cronApi) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
panic("implement me")
}
func (c cronApi) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
panic("implement me")
}
func (c cronApi) Get(ctx context.Context, name string, opts v1.GetOptions) (*cronjobv1.CronJob, error) {
panic("implement me")
}
func (c cronApi) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
panic("implement me")
}
func (c cronApi) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *cronjobv1.CronJob, err error) {
panic("implement me")
}
func (c cronApi) Apply(ctx context.Context, cronJob *applyv1.CronJobApplyConfiguration, opts v1.ApplyOptions) (result *cronjobv1.CronJob, err error) {
panic("implement me")
}
func (c cronApi) ApplyStatus(ctx context.Context, cronJob *applyv1.CronJobApplyConfiguration, opts v1.ApplyOptions) (result *cronjobv1.CronJob, err error) {
panic("implement me")
}
var _ batchv1.CronJobInterface = &cronApi{}
func boolP(b bool) *bool {
return &b
}