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 }