github.com/nais/knorten@v0.0.0-20240104110906-55926958e361/pkg/api/chart_test.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"reflect"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/nais/knorten/pkg/chart"
    17  	"github.com/nais/knorten/pkg/database"
    18  	"github.com/nais/knorten/pkg/database/gensql"
    19  )
    20  
    21  func TestJupyterAPI(t *testing.T) {
    22  	ctx := context.Background()
    23  
    24  	team, err := prepareChartTests(ctx, "jupyter-team")
    25  	if err != nil {
    26  		t.Fatalf("preparing jupyter chart tests: %v", err)
    27  	}
    28  
    29  	t.Cleanup(func() {
    30  		if err := repo.TeamDelete(ctx, team.ID); err != nil {
    31  			t.Errorf("cleaning up after jupyter tests %v", err)
    32  		}
    33  	})
    34  
    35  	t.Run("get new jupyterhub html", func(t *testing.T) {
    36  		resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/jupyterhub/new", server.URL, team.Slug))
    37  		if err != nil {
    38  			t.Error(err)
    39  		}
    40  		defer resp.Body.Close()
    41  
    42  		if resp.StatusCode != http.StatusOK {
    43  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
    44  		}
    45  
    46  		if resp.Header.Get("Content-Type") != htmlContentType {
    47  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
    48  		}
    49  
    50  		received, err := io.ReadAll(resp.Body)
    51  		if err != nil {
    52  			t.Error(err)
    53  		}
    54  		receivedMinimized, err := minimizeHTML(string(received))
    55  		if err != nil {
    56  			t.Error(err)
    57  		}
    58  
    59  		expected, err := createExpectedHTML("charts/jupyterhub", map[string]any{
    60  			"team": team.Slug,
    61  		})
    62  		if err != nil {
    63  			t.Error(err)
    64  		}
    65  		expectedMinimized, err := minimizeHTML(expected)
    66  		if err != nil {
    67  			t.Error(err)
    68  		}
    69  
    70  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
    71  			t.Errorf("mismatch (-want +got):\n%s", diff)
    72  		}
    73  	})
    74  
    75  	t.Run("create new jupyterhub", func(t *testing.T) {
    76  		cpu := "1.0"
    77  		memory := "2G"
    78  		culltimeout := "3600"
    79  
    80  		data := url.Values{"cpu": {cpu}, "memory": {memory}, "imagename": {""}, "imagetag": {""}, "culltimeout": {culltimeout}}
    81  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/jupyterhub/new", server.URL, team.Slug), data)
    82  		if err != nil {
    83  			t.Error(err)
    84  		}
    85  		defer resp.Body.Close()
    86  
    87  		events, err := repo.EventsGetType(ctx, database.EventTypeCreateJupyter)
    88  		if err != nil {
    89  			t.Error(err)
    90  		}
    91  
    92  		eventPayload, err := getEventForJupyterhub(events, team.ID)
    93  		if err != nil {
    94  			t.Error(err)
    95  		}
    96  
    97  		if eventPayload.TeamID == "" {
    98  			t.Errorf("create jupyterhub: no event registered for team %v", team.ID)
    99  		}
   100  		if eventPayload.CPU != cpu {
   101  			t.Errorf("create jupyterhub: cpu value - expected %v, got %v", cpu, eventPayload.CPU)
   102  		}
   103  
   104  		if eventPayload.Memory != memory {
   105  			t.Errorf("create jupyterhub: memory value - expected %v, got %v", memory, eventPayload.Memory)
   106  		}
   107  
   108  		if eventPayload.CullTimeout != culltimeout {
   109  			t.Errorf("create jupyterhub: culltimeout value - expected %v, got %v", culltimeout, eventPayload.CullTimeout)
   110  		}
   111  
   112  		if len(eventPayload.UserIdents) != 3 {
   113  			t.Errorf("create jupyterhub: expected 3 users, got %v", len(eventPayload.UserIdents))
   114  		}
   115  	})
   116  
   117  	expectedValues := chart.JupyterConfigurableValues{
   118  		TeamID:      team.ID,
   119  		CPU:         "1.0",
   120  		Memory:      "1G",
   121  		CullTimeout: "3600",
   122  	}
   123  
   124  	if err := createChartForTeam(ctx, team.ID, expectedValues, gensql.ChartTypeJupyterhub); err != nil {
   125  		t.Error(err)
   126  	}
   127  
   128  	t.Run("get edit jupyterhub html", func(t *testing.T) {
   129  		resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/jupyterhub/edit", server.URL, team.Slug))
   130  		if err != nil {
   131  			t.Error(err)
   132  		}
   133  		defer resp.Body.Close()
   134  
   135  		if resp.StatusCode != http.StatusOK {
   136  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   137  		}
   138  
   139  		if resp.Header.Get("Content-Type") != htmlContentType {
   140  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   141  		}
   142  
   143  		received, err := io.ReadAll(resp.Body)
   144  		if err != nil {
   145  			t.Error(err)
   146  		}
   147  		receivedMinimized, err := minimizeHTML(string(received))
   148  		if err != nil {
   149  			t.Error(err)
   150  		}
   151  
   152  		expected, err := createExpectedHTML("charts/jupyterhub", map[string]any{
   153  			"team": team.Slug,
   154  			"values": &jupyterForm{
   155  				CPU:         expectedValues.CPU,
   156  				Memory:      expectedValues.Memory,
   157  				CullTimeout: expectedValues.CullTimeout,
   158  			},
   159  		})
   160  		if err != nil {
   161  			t.Error(err)
   162  		}
   163  		expectedMinimized, err := minimizeHTML(expected)
   164  		if err != nil {
   165  			t.Error(err)
   166  		}
   167  
   168  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   169  			t.Errorf("mismatch (-want +got):\n%s", diff)
   170  		}
   171  	})
   172  
   173  	t.Run("edit jupyterhub", func(t *testing.T) {
   174  		newCPU := "2.0"
   175  		newMemory := "2G"
   176  		imageName := "ghcr.io/org/repo/image"
   177  		imageTag := "v1"
   178  		newCullTimeout := "7200"
   179  		data := url.Values{"cpu": {newCPU}, "memory": {newMemory}, "imagename": {imageName}, "imagetag": {imageTag}, "culltimeout": {newCullTimeout}}
   180  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/jupyterhub/edit", server.URL, team.Slug), data)
   181  		if err != nil {
   182  			t.Error(err)
   183  		}
   184  		defer resp.Body.Close()
   185  
   186  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   187  		if err != nil {
   188  			t.Error(err)
   189  		}
   190  
   191  		eventPayload, err := getEventForJupyterhub(events, team.ID)
   192  		if err != nil {
   193  			t.Error(err)
   194  		}
   195  
   196  		if eventPayload.TeamID == "" {
   197  			t.Errorf("create jupyterhub: no event registered for team %v", team.ID)
   198  		}
   199  
   200  		if eventPayload.CPU != newCPU {
   201  			t.Errorf("create jupyterhub: cpu value - expected %v, got %v", newCPU, eventPayload.CPU)
   202  		}
   203  
   204  		if eventPayload.Memory != newMemory {
   205  			t.Errorf("create jupyterhub: memory value - expected %v, got %v", newMemory, eventPayload.Memory)
   206  		}
   207  
   208  		if eventPayload.CullTimeout != newCullTimeout {
   209  			t.Errorf("create jupyterhub: culltimeout value - expected %v, got %v", newCullTimeout, eventPayload.CullTimeout)
   210  		}
   211  
   212  		if eventPayload.ImageName != imageName {
   213  			t.Errorf("create jupyterhub: image name value - expected %v, got %v", imageName, eventPayload.ImageName)
   214  		}
   215  
   216  		if eventPayload.ImageTag != imageTag {
   217  			t.Errorf("create jupyterhub: image tag value - expected %v, got %v", imageTag, eventPayload.ImageTag)
   218  		}
   219  
   220  		if len(eventPayload.UserIdents) != 3 {
   221  			t.Errorf("create jupyterhub: expected 3 users, got %v", len(eventPayload.UserIdents))
   222  		}
   223  	})
   224  
   225  	t.Run("delete jupyterhub", func(t *testing.T) {
   226  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/jupyterhub/delete", server.URL, team.Slug), nil)
   227  		if err != nil {
   228  			t.Error(err)
   229  		}
   230  		resp.Body.Close()
   231  
   232  		if resp.StatusCode != http.StatusOK {
   233  			t.Errorf("delete team: expected status code 200, got %v", resp.StatusCode)
   234  		}
   235  
   236  		events, err := repo.EventsGetType(ctx, database.EventTypeDeleteJupyter)
   237  		if err != nil {
   238  			t.Error(err)
   239  		}
   240  
   241  		if !deleteEventCreatedForTeam(events, team.ID) {
   242  			t.Errorf("delete jupyterhub: no event registered for team %v", team.ID)
   243  		}
   244  	})
   245  }
   246  
   247  func TestAirflowAPI(t *testing.T) {
   248  	ctx := context.Background()
   249  
   250  	team, err := prepareChartTests(ctx, "airflow-team")
   251  	if err != nil {
   252  		t.Errorf("preparing airflow chart tests: %v", err)
   253  	}
   254  
   255  	t.Cleanup(func() {
   256  		if err := repo.TeamDelete(ctx, team.ID); err != nil {
   257  			t.Errorf("cleaning up after airflow tests %v", err)
   258  		}
   259  	})
   260  
   261  	t.Run("get new airflow html", func(t *testing.T) {
   262  		resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/airflow/new", server.URL, team.Slug))
   263  		if err != nil {
   264  			t.Error(err)
   265  		}
   266  		defer resp.Body.Close()
   267  
   268  		if resp.StatusCode != http.StatusOK {
   269  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   270  		}
   271  
   272  		if resp.Header.Get("Content-Type") != htmlContentType {
   273  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   274  		}
   275  
   276  		received, err := io.ReadAll(resp.Body)
   277  		if err != nil {
   278  			t.Error(err)
   279  		}
   280  		receivedMinimized, err := minimizeHTML(string(received))
   281  		if err != nil {
   282  			t.Error(err)
   283  		}
   284  
   285  		expected, err := createExpectedHTML("charts/airflow", map[string]any{
   286  			"team": team.Slug,
   287  		})
   288  		if err != nil {
   289  			t.Error(err)
   290  		}
   291  		expectedMinimized, err := minimizeHTML(expected)
   292  		if err != nil {
   293  			t.Error(err)
   294  		}
   295  
   296  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   297  			t.Errorf("mismatch (-want +got):\n%s", diff)
   298  		}
   299  	})
   300  
   301  	t.Run("create new airflow", func(t *testing.T) {
   302  		dagRepo := "navikt/repo"
   303  		dagRepoBranch := "main"
   304  
   305  		data := url.Values{"dagrepo": {dagRepo}, "dagrepobranch": {dagRepoBranch}, "restrictairflowegress": {""}}
   306  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/airflow/new", server.URL, team.Slug), data)
   307  		if err != nil {
   308  			t.Error(err)
   309  		}
   310  		defer resp.Body.Close()
   311  
   312  		events, err := repo.EventsGetType(ctx, database.EventTypeCreateAirflow)
   313  		if err != nil {
   314  			t.Error(err)
   315  		}
   316  
   317  		eventPayload, err := getEventForAirflow(events, team.ID)
   318  		if err != nil {
   319  			t.Error(err)
   320  		}
   321  
   322  		if eventPayload.TeamID == "" {
   323  			t.Errorf("create airflow: no event registered for team %v", team.ID)
   324  		}
   325  
   326  		if eventPayload.DagRepo != dagRepo {
   327  			t.Errorf("create airflow: dag repo value, expected %v, got %v", dagRepo, eventPayload.DagRepo)
   328  		}
   329  
   330  		if eventPayload.DagRepoBranch != dagRepoBranch {
   331  			t.Errorf("create airflow: dag repo branch value, expected %v, got %v", dagRepoBranch, eventPayload.DagRepoBranch)
   332  		}
   333  
   334  		if eventPayload.RestrictEgress {
   335  			t.Errorf("create airflow: restrict egress value, expected %v, got %v", false, eventPayload.RestrictEgress)
   336  		}
   337  	})
   338  
   339  	dagRepo := "navikt/repo"
   340  	branch := "main"
   341  	expectedRestrictEgress := false
   342  	expectedValues := chart.AirflowConfigurableValues{
   343  		TeamID:         team.ID,
   344  		DagRepo:        dagRepo,
   345  		DagRepoBranch:  branch,
   346  		RestrictEgress: expectedRestrictEgress,
   347  	}
   348  
   349  	if err := createChartForTeam(ctx, team.ID, expectedValues, gensql.ChartTypeAirflow); err != nil {
   350  		t.Error(err)
   351  	}
   352  	if err := repo.TeamChartValueInsert(ctx, chart.TeamValueKeyRestrictEgress, strconv.FormatBool(expectedRestrictEgress), team.ID, gensql.ChartTypeAirflow); err != nil {
   353  		t.Error(err)
   354  	}
   355  
   356  	t.Run("get edit airflow html", func(t *testing.T) {
   357  		resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/airflow/edit", server.URL, team.Slug))
   358  		if err != nil {
   359  			t.Error(err)
   360  		}
   361  		defer resp.Body.Close()
   362  
   363  		if resp.StatusCode != http.StatusOK {
   364  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   365  		}
   366  
   367  		if resp.Header.Get("Content-Type") != htmlContentType {
   368  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   369  		}
   370  
   371  		received, err := io.ReadAll(resp.Body)
   372  		if err != nil {
   373  			t.Error(err)
   374  		}
   375  		receivedMinimized, err := minimizeHTML(string(received))
   376  		if err != nil {
   377  			t.Error(err)
   378  		}
   379  
   380  		expected, err := createExpectedHTML("charts/airflow", map[string]any{
   381  			"team": team.Slug,
   382  			"values": &airflowForm{
   383  				DagRepo:       expectedValues.DagRepo,
   384  				DagRepoBranch: expectedValues.DagRepoBranch,
   385  			},
   386  		})
   387  		if err != nil {
   388  			t.Error(err)
   389  		}
   390  		expectedMinimized, err := minimizeHTML(expected)
   391  		if err != nil {
   392  			t.Error(err)
   393  		}
   394  
   395  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   396  			t.Errorf("mismatch (-want +got):\n%s", diff)
   397  		}
   398  	})
   399  
   400  	t.Run("edit airflow", func(t *testing.T) {
   401  		newDagRepo := "navikt/newrepo"
   402  		newDagRepoBranch := "master"
   403  		customImage := "ghcr.io/navikt/myimage:v1"
   404  
   405  		data := url.Values{"dagrepo": {newDagRepo}, "dagrepobranch": {newDagRepoBranch}, "airflowimage": {customImage}}
   406  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/airflow/edit", server.URL, team.Slug), data)
   407  		if err != nil {
   408  			t.Error(err)
   409  		}
   410  		defer resp.Body.Close()
   411  
   412  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   413  		if err != nil {
   414  			t.Error(err)
   415  		}
   416  
   417  		eventPayload, err := getEventForAirflow(events, team.ID)
   418  		if err != nil {
   419  			t.Error(err)
   420  		}
   421  
   422  		if eventPayload.TeamID == "" {
   423  			t.Errorf("edit airflow: no event registered for team %v", team.ID)
   424  		}
   425  
   426  		if eventPayload.DagRepo != newDagRepo {
   427  			t.Errorf("edit airflow: dag repo value, expected %v, got %v", newDagRepo, eventPayload.DagRepo)
   428  		}
   429  
   430  		if eventPayload.DagRepoBranch != newDagRepoBranch {
   431  			t.Errorf("edit airflow: dag repo branch value, expected %v, got %v", newDagRepoBranch, eventPayload.DagRepoBranch)
   432  		}
   433  
   434  		if strings.Join([]string{eventPayload.AirflowImage, eventPayload.AirflowTag}, ":") != customImage {
   435  			t.Errorf("edit airflow: custom image, expected %v, got %v", customImage, strings.Join([]string{eventPayload.AirflowImage, eventPayload.AirflowTag}, ":"))
   436  		}
   437  	})
   438  
   439  	t.Run("delete airflow", func(t *testing.T) {
   440  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/airflow/delete", server.URL, team.Slug), nil)
   441  		if err != nil {
   442  			t.Error(err)
   443  		}
   444  		resp.Body.Close()
   445  
   446  		if resp.StatusCode != http.StatusOK {
   447  			t.Errorf("delete team: expected status code 200, got %v", resp.StatusCode)
   448  		}
   449  
   450  		events, err := repo.EventsGetType(ctx, database.EventTypeDeleteAirflow)
   451  		if err != nil {
   452  			t.Error(err)
   453  		}
   454  
   455  		if !deleteEventCreatedForTeam(events, team.ID) {
   456  			t.Errorf("delete airflow: no event registered for team %v", team.ID)
   457  		}
   458  	})
   459  }
   460  
   461  func prepareChartTests(ctx context.Context, teamName string) (gensql.Team, error) {
   462  	team := gensql.Team{
   463  		ID:    teamName + "-1234",
   464  		Slug:  teamName,
   465  		Users: []string{testUser.Email, "user.one@nav.no", "user.two@nav.no"},
   466  	}
   467  
   468  	return team, repo.TeamCreate(ctx, team)
   469  }
   470  
   471  func getEventForJupyterhub(events []gensql.Event, team string) (chart.JupyterConfigurableValues, error) {
   472  	for _, event := range events {
   473  		payload := chart.JupyterConfigurableValues{}
   474  		err := json.Unmarshal(event.Payload, &payload)
   475  		if err != nil {
   476  			return chart.JupyterConfigurableValues{}, err
   477  		}
   478  
   479  		if payload.TeamID == team {
   480  			return payload, nil
   481  		}
   482  	}
   483  
   484  	return chart.JupyterConfigurableValues{}, nil
   485  }
   486  
   487  func getEventForAirflow(events []gensql.Event, team string) (chart.AirflowConfigurableValues, error) {
   488  	for _, event := range events {
   489  		payload := chart.AirflowConfigurableValues{}
   490  		err := json.Unmarshal(event.Payload, &payload)
   491  		if err != nil {
   492  			return chart.AirflowConfigurableValues{}, err
   493  		}
   494  
   495  		if payload.TeamID == team {
   496  			return payload, nil
   497  		}
   498  	}
   499  
   500  	return chart.AirflowConfigurableValues{}, nil
   501  }
   502  
   503  func createChartForTeam(ctx context.Context, teamID string, chartValues any, chartType gensql.ChartType) error {
   504  	values := reflect.ValueOf(chartValues)
   505  	for i := 0; i < values.NumField(); i++ {
   506  		key := values.Type().Field(i).Tag.Get("helm")
   507  		value := values.Field(i).Interface()
   508  		if key != "" && value != "" {
   509  			if err := repo.TeamChartValueInsert(ctx, key, value.(string), teamID, chartType); err != nil {
   510  				return err
   511  			}
   512  		}
   513  	}
   514  
   515  	return nil
   516  }