github.com/navikt/knorten@v0.0.0-20240419132333-1333f46ed8b6/pkg/api/admin_test.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/navikt/knorten/pkg/database"
    14  	"github.com/navikt/knorten/pkg/database/gensql"
    15  	"github.com/navikt/knorten/pkg/k8s"
    16  )
    17  
    18  func TestAdminAPI(t *testing.T) {
    19  	ctx := context.Background()
    20  	teams, err := prepareAdminTests(ctx)
    21  	if err != nil {
    22  		t.Fatalf("preparing admin tests: %v", err)
    23  	}
    24  	t.Cleanup(func() {
    25  		if err := cleanUpAdminTests(ctx, teams); err != nil {
    26  			t.Errorf("cleaning up after admin tests: %v", err)
    27  		}
    28  	})
    29  
    30  	t.Run("get admin panel html", func(t *testing.T) {
    31  		resp, err := server.Client().Get(fmt.Sprintf("%v/admin", server.URL))
    32  		if err != nil {
    33  			t.Error(err)
    34  		}
    35  		defer resp.Body.Close()
    36  
    37  		if resp.StatusCode != http.StatusOK {
    38  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
    39  		}
    40  
    41  		if resp.Header.Get("Content-Type") != htmlContentType {
    42  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
    43  		}
    44  
    45  		received, err := io.ReadAll(resp.Body)
    46  		if err != nil {
    47  			t.Error(err)
    48  		}
    49  		receivedMinimized, err := minimizeHTML(string(received))
    50  		if err != nil {
    51  			t.Error(err)
    52  		}
    53  
    54  		eventsTeamA, err := repo.EventsByOwnerGet(ctx, teams[0].ID, 1)
    55  		if err != nil {
    56  			t.Error(err)
    57  		}
    58  
    59  		eventsTeamB, err := repo.EventsByOwnerGet(ctx, teams[1].ID, 1)
    60  		if err != nil {
    61  			t.Error(err)
    62  		}
    63  
    64  		expected, err := createExpectedHTML("admin/index", map[string]any{
    65  			"teams": []teamInfo{
    66  				{
    67  					Team:      teams[0],
    68  					Namespace: k8s.TeamIDToNamespace(teams[0].ID),
    69  					Apps: []gensql.ChartType{
    70  						gensql.ChartTypeJupyterhub,
    71  					},
    72  					Events: eventsTeamA,
    73  				},
    74  				{
    75  					Team:      teams[1],
    76  					Namespace: k8s.TeamIDToNamespace(teams[1].ID),
    77  					Apps: []gensql.ChartType{
    78  						gensql.ChartTypeJupyterhub,
    79  						gensql.ChartTypeAirflow,
    80  					},
    81  					Events: eventsTeamB,
    82  				},
    83  			},
    84  		})
    85  		if err != nil {
    86  			t.Error(err)
    87  		}
    88  		expectedMinimized, err := minimizeHTML(expected)
    89  		if err != nil {
    90  			t.Error(err)
    91  		}
    92  
    93  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
    94  			t.Errorf("mismatch (-want +got):\n%s", diff)
    95  		}
    96  	})
    97  
    98  	t.Run("get admin panel jupyter values html", func(t *testing.T) {
    99  		resp, err := server.Client().Get(fmt.Sprintf("%v/admin/jupyterhub", server.URL))
   100  		if err != nil {
   101  			t.Error(err)
   102  		}
   103  		defer resp.Body.Close()
   104  
   105  		if resp.StatusCode != http.StatusOK {
   106  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   107  		}
   108  
   109  		if resp.Header.Get("Content-Type") != htmlContentType {
   110  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   111  		}
   112  
   113  		received, err := io.ReadAll(resp.Body)
   114  		if err != nil {
   115  			t.Error(err)
   116  		}
   117  		receivedMinimized, err := minimizeHTML(string(received))
   118  		if err != nil {
   119  			t.Error(err)
   120  		}
   121  
   122  		expected, err := createExpectedHTML("admin/chart", map[string]any{
   123  			"chart": string(gensql.ChartTypeJupyterhub),
   124  			"values": []gensql.ChartGlobalValue{
   125  				{
   126  					Key:   "jupytervalue",
   127  					Value: "value",
   128  				},
   129  			},
   130  		})
   131  		if err != nil {
   132  			t.Error(err)
   133  		}
   134  		expectedMinimized, err := minimizeHTML(expected)
   135  		if err != nil {
   136  			t.Error(err)
   137  		}
   138  
   139  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   140  			t.Errorf("mismatch (-want +got):\n%s", diff)
   141  		}
   142  	})
   143  
   144  	t.Run("update jupyter global values get confirm html", func(t *testing.T) {
   145  		// Disable automatic redirect. For the test we need to add the session cookie to the subsequent GET request for the confirm html manually
   146  		server.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error {
   147  			return http.ErrUseLastResponse
   148  		}
   149  		t.Cleanup(func() {
   150  			server.Client().CheckRedirect = nil
   151  		})
   152  
   153  		data := url.Values{"jupytervalue": {"updated"}, "key.0": {"new"}, "value.0": {"new"}}
   154  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/jupyterhub", server.URL), data)
   155  		if err != nil {
   156  			t.Error(err)
   157  		}
   158  		defer resp.Body.Close()
   159  
   160  		if resp.StatusCode != http.StatusSeeOther {
   161  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusSeeOther)
   162  		}
   163  
   164  		sessionCookie, err := getSessionCookieFromResponse(resp)
   165  		if err != nil {
   166  			t.Error(err)
   167  		}
   168  
   169  		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%v/admin/jupyterhub/confirm", server.URL), nil)
   170  		if err != nil {
   171  			t.Error(err)
   172  		}
   173  		req.AddCookie(sessionCookie)
   174  		resp, err = server.Client().Do(req)
   175  		if err != nil {
   176  			t.Error(err)
   177  		}
   178  		defer resp.Body.Close()
   179  
   180  		if resp.StatusCode != http.StatusOK {
   181  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   182  		}
   183  
   184  		received, err := io.ReadAll(resp.Body)
   185  		if err != nil {
   186  			t.Error(err)
   187  		}
   188  		receivedMinimized, err := minimizeHTML(string(received))
   189  		if err != nil {
   190  			t.Error(err)
   191  		}
   192  
   193  		expected, err := createExpectedHTML("admin/confirm", map[string]any{
   194  			"chart": string(gensql.ChartTypeJupyterhub),
   195  			"changedValues": []map[string]diffValue{
   196  				{
   197  					"jupytervalue": {
   198  						Old: "value",
   199  						New: "updated",
   200  					},
   201  					"new": {
   202  						New: "new",
   203  					},
   204  				},
   205  			},
   206  		})
   207  		if err != nil {
   208  			t.Error(err)
   209  		}
   210  		expectedMinimized, err := minimizeHTML(expected)
   211  		if err != nil {
   212  			t.Error(err)
   213  		}
   214  
   215  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   216  			t.Errorf("mismatch (-want +got):\n%s", diff)
   217  		}
   218  	})
   219  
   220  	t.Run("update jupyter global values", func(t *testing.T) {
   221  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   222  		if err != nil {
   223  			t.Error(err)
   224  		}
   225  
   226  		data := url.Values{"jupytervalue": {"updated"}, "new": {"new"}}
   227  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/jupyterhub/confirm", server.URL), data)
   228  		if err != nil {
   229  			t.Error(err)
   230  		}
   231  		defer resp.Body.Close()
   232  
   233  		if resp.StatusCode != http.StatusOK {
   234  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   235  		}
   236  
   237  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   238  		if err != nil {
   239  			t.Error(err)
   240  		}
   241  
   242  		newEvents := getNewEvents(oldEvents, events)
   243  		for _, team := range teams {
   244  			eventPayload, err := getEventForJupyterhub(newEvents, team.ID)
   245  			if err != nil {
   246  				t.Error(err)
   247  			}
   248  
   249  			if eventPayload.TeamID == "" {
   250  				t.Errorf("update admin values: no update jupyterhub event registered for team %v", team.ID)
   251  			}
   252  		}
   253  	})
   254  
   255  	t.Run("sync jupyterhub chart for team", func(t *testing.T) {
   256  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   257  		if err != nil {
   258  			t.Error(err)
   259  		}
   260  
   261  		data := url.Values{"team": {"team-a-1234"}}
   262  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/jupyterhub/sync", server.URL), data)
   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  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   273  		if err != nil {
   274  			t.Error(err)
   275  		}
   276  
   277  		newEvents := getNewEvents(oldEvents, events)
   278  		eventPayload, err := getEventForJupyterhub(newEvents, teams[0].ID)
   279  		if err != nil {
   280  			t.Error(err)
   281  		}
   282  
   283  		if eventPayload.TeamID == "" {
   284  			t.Errorf("sync chart: no update jupyterhub event registered for team %v", teams[1].ID)
   285  		}
   286  	})
   287  
   288  	t.Run("sync all jupyterhub charts", func(t *testing.T) {
   289  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   290  		if err != nil {
   291  			t.Error(err)
   292  		}
   293  
   294  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/jupyterhub/sync/all", server.URL), nil)
   295  		if err != nil {
   296  			t.Error(err)
   297  		}
   298  		defer resp.Body.Close()
   299  
   300  		if resp.StatusCode != http.StatusOK {
   301  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   302  		}
   303  
   304  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter)
   305  		if err != nil {
   306  			t.Error(err)
   307  		}
   308  
   309  		newEvents := getNewEvents(oldEvents, events)
   310  		for _, team := range teams {
   311  			eventPayload, err := getEventForJupyterhub(newEvents, team.ID)
   312  			if err != nil {
   313  				t.Error(err)
   314  			}
   315  
   316  			if eventPayload.TeamID == "" {
   317  				t.Errorf("sync all jupyterhub charts: no update jupyterhub event registered for team %v", team.ID)
   318  			}
   319  		}
   320  	})
   321  
   322  	t.Run("get admin panel airflow values html", func(t *testing.T) {
   323  		resp, err := server.Client().Get(fmt.Sprintf("%v/admin/airflow", server.URL))
   324  		if err != nil {
   325  			t.Error(err)
   326  		}
   327  		defer resp.Body.Close()
   328  
   329  		if resp.StatusCode != http.StatusOK {
   330  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   331  		}
   332  
   333  		if resp.Header.Get("Content-Type") != htmlContentType {
   334  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   335  		}
   336  
   337  		received, err := io.ReadAll(resp.Body)
   338  		if err != nil {
   339  			t.Error(err)
   340  		}
   341  		receivedMinimized, err := minimizeHTML(string(received))
   342  		if err != nil {
   343  			t.Error(err)
   344  		}
   345  
   346  		expected, err := createExpectedHTML("admin/chart", map[string]any{
   347  			"chart": string(gensql.ChartTypeAirflow),
   348  			"values": []gensql.ChartGlobalValue{
   349  				{
   350  					Key:   "airflowvalue",
   351  					Value: "value",
   352  				},
   353  			},
   354  		})
   355  		if err != nil {
   356  			t.Error(err)
   357  		}
   358  		expectedMinimized, err := minimizeHTML(expected)
   359  		if err != nil {
   360  			t.Error(err)
   361  		}
   362  
   363  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   364  			t.Errorf("mismatch (-want +got):\n%s", diff)
   365  		}
   366  	})
   367  
   368  	t.Run("update airflow global values", func(t *testing.T) {
   369  		// Disable automatic redirect. For the test we need to add the session cookie to the subsequent GET request for the confirm html manually
   370  		server.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error {
   371  			return http.ErrUseLastResponse
   372  		}
   373  		t.Cleanup(func() {
   374  			server.Client().CheckRedirect = nil
   375  		})
   376  
   377  		data := url.Values{"airflowvalue": {"updated"}, "key.0": {"new"}, "value.0": {"new"}}
   378  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/airflow", server.URL), data)
   379  		if err != nil {
   380  			t.Error(err)
   381  		}
   382  		defer resp.Body.Close()
   383  
   384  		if resp.StatusCode != http.StatusSeeOther {
   385  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusSeeOther)
   386  		}
   387  
   388  		sessionCookie, err := getSessionCookieFromResponse(resp)
   389  		if err != nil {
   390  			t.Error(err)
   391  		}
   392  
   393  		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%v/admin/airflow/confirm", server.URL), nil)
   394  		if err != nil {
   395  			t.Error(err)
   396  		}
   397  		req.AddCookie(sessionCookie)
   398  		resp, err = server.Client().Do(req)
   399  		if err != nil {
   400  			t.Error(err)
   401  		}
   402  		defer resp.Body.Close()
   403  
   404  		if resp.StatusCode != http.StatusOK {
   405  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   406  		}
   407  
   408  		received, err := io.ReadAll(resp.Body)
   409  		if err != nil {
   410  			t.Error(err)
   411  		}
   412  		receivedMinimized, err := minimizeHTML(string(received))
   413  		if err != nil {
   414  			t.Error(err)
   415  		}
   416  
   417  		expected, err := createExpectedHTML("admin/confirm", map[string]any{
   418  			"chart": string(gensql.ChartTypeAirflow),
   419  			"changedValues": []map[string]diffValue{
   420  				{
   421  					"airflowvalue": {
   422  						Old: "value",
   423  						New: "updated",
   424  					},
   425  					"new": {
   426  						New: "new",
   427  					},
   428  				},
   429  			},
   430  		})
   431  		if err != nil {
   432  			t.Error(err)
   433  		}
   434  		expectedMinimized, err := minimizeHTML(expected)
   435  		if err != nil {
   436  			t.Error(err)
   437  		}
   438  
   439  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   440  			t.Errorf("mismatch (-want +got):\n%s", diff)
   441  		}
   442  	})
   443  
   444  	t.Run("update airflow global values", func(t *testing.T) {
   445  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   446  		if err != nil {
   447  			t.Error(err)
   448  		}
   449  
   450  		data := url.Values{"airflowvalue": {"updated"}, "new": {"new"}}
   451  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/airflow/confirm", server.URL), data)
   452  		if err != nil {
   453  			t.Error(err)
   454  		}
   455  		defer resp.Body.Close()
   456  
   457  		if resp.StatusCode != http.StatusOK {
   458  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   459  		}
   460  
   461  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   462  		if err != nil {
   463  			t.Error(err)
   464  		}
   465  
   466  		newEvents := getNewEvents(oldEvents, events)
   467  		eventPayload, err := getEventForAirflow(newEvents, teams[1].ID)
   468  		if err != nil {
   469  			t.Error(err)
   470  		}
   471  
   472  		if eventPayload.TeamID == "" {
   473  			t.Errorf("update airflow global values: no update airflow event registered for team %v", teams[1].ID)
   474  		}
   475  	})
   476  
   477  	t.Run("sync airflow chart for team", func(t *testing.T) {
   478  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   479  		if err != nil {
   480  			t.Error(err)
   481  		}
   482  
   483  		data := url.Values{"team": {"team-b-1234"}}
   484  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/airflow/sync", server.URL), data)
   485  		if err != nil {
   486  			t.Error(err)
   487  		}
   488  		defer resp.Body.Close()
   489  
   490  		if resp.StatusCode != http.StatusOK {
   491  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   492  		}
   493  
   494  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   495  		if err != nil {
   496  			t.Error(err)
   497  		}
   498  
   499  		newEvents := getNewEvents(oldEvents, events)
   500  		eventPayload, err := getEventForAirflow(newEvents, teams[1].ID)
   501  		if err != nil {
   502  			t.Error(err)
   503  		}
   504  
   505  		if eventPayload.TeamID == "" {
   506  			t.Errorf("sync airflow chart for team: no update airflow event registered for team %v", teams[1].ID)
   507  		}
   508  	})
   509  
   510  	t.Run("sync all airflow charts", func(t *testing.T) {
   511  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   512  		if err != nil {
   513  			t.Error(err)
   514  		}
   515  
   516  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/airflow/sync/all", server.URL), nil)
   517  		if err != nil {
   518  			t.Error(err)
   519  		}
   520  		defer resp.Body.Close()
   521  
   522  		if resp.StatusCode != http.StatusOK {
   523  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   524  		}
   525  
   526  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow)
   527  		if err != nil {
   528  			t.Error(err)
   529  		}
   530  
   531  		newEvents := getNewEvents(oldEvents, events)
   532  		eventPayload, err := getEventForAirflow(newEvents, teams[0].ID)
   533  		if err != nil {
   534  			t.Error(err)
   535  		}
   536  		if eventPayload.TeamID != "" {
   537  			t.Errorf("sync all airflow charts: airflow event registered for team %v eventhough team does not have airflow", teams[0].ID)
   538  		}
   539  
   540  		eventPayload, err = getEventForAirflow(newEvents, teams[1].ID)
   541  		if err != nil {
   542  			t.Error(err)
   543  		}
   544  		if eventPayload.TeamID == "" {
   545  			t.Errorf("sync all airflow charts: no update airflow event registered for team %v", teams[1].ID)
   546  		}
   547  	})
   548  
   549  	t.Run("sync all teams", func(t *testing.T) {
   550  		oldEvents, err := repo.EventsGetType(ctx, database.EventTypeUpdateTeam)
   551  		if err != nil {
   552  			t.Error(err)
   553  		}
   554  
   555  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/team/sync/all", server.URL), nil)
   556  		if err != nil {
   557  			t.Error(err)
   558  		}
   559  		defer resp.Body.Close()
   560  
   561  		if resp.StatusCode != http.StatusOK {
   562  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   563  		}
   564  
   565  		events, err := repo.EventsGetType(ctx, database.EventTypeUpdateTeam)
   566  		if err != nil {
   567  			t.Error(err)
   568  		}
   569  
   570  		newEvents := getNewEvents(oldEvents, events)
   571  		for _, team := range teams {
   572  			eventPayload, err := getEventForTeam(newEvents, team.Slug)
   573  			if err != nil {
   574  				t.Error(err)
   575  			}
   576  			if eventPayload.ID == "" {
   577  				t.Errorf("sync all teams: no update team event registered for team %v", team.Slug)
   578  			}
   579  		}
   580  	})
   581  
   582  	t.Run("delete team", func(t *testing.T) {
   583  		team := gensql.Team{
   584  			ID:    "delete-me-1234",
   585  			Slug:  "delete-me",
   586  			Users: []string{testUser.Email},
   587  		}
   588  		if err := repo.TeamCreate(ctx, team); err != nil {
   589  			t.Error(err)
   590  		}
   591  		t.Cleanup(func() {
   592  			if err := repo.TeamDelete(ctx, team.ID); err != nil {
   593  				t.Error(err)
   594  			}
   595  		})
   596  
   597  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/team/%v/delete", server.URL, team.Slug), nil)
   598  		if err != nil {
   599  			t.Error(err)
   600  		}
   601  		resp.Body.Close()
   602  
   603  		if resp.StatusCode != http.StatusOK {
   604  			t.Errorf("delete team: expected status code 200, got %v", resp.StatusCode)
   605  		}
   606  
   607  		events, err := repo.EventsGetType(ctx, database.EventTypeDeleteTeam)
   608  		if err != nil {
   609  			t.Error(err)
   610  		}
   611  
   612  		if !deleteEventCreatedForTeam(events, team.ID) {
   613  			t.Errorf("delete team: no event registered for team %v", team.ID)
   614  		}
   615  	})
   616  
   617  	t.Run("get event html", func(t *testing.T) {
   618  		events, err := repo.EventsByOwnerGet(ctx, teams[0].ID, 1)
   619  		if err != nil {
   620  			t.Error(err)
   621  		}
   622  		if len(events) == 0 {
   623  			t.Errorf("get event html: no event found for team %v", teams[0].ID)
   624  		}
   625  
   626  		resp, err := server.Client().Get(fmt.Sprintf("%v/admin/event/%v", server.URL, events[0].ID))
   627  		if err != nil {
   628  			t.Error(err)
   629  		}
   630  		defer resp.Body.Close()
   631  
   632  		if resp.StatusCode != http.StatusOK {
   633  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   634  		}
   635  
   636  		if resp.Header.Get("Content-Type") != htmlContentType {
   637  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   638  		}
   639  
   640  		received, err := io.ReadAll(resp.Body)
   641  		if err != nil {
   642  			t.Error(err)
   643  		}
   644  		receivedMinimized, err := minimizeHTML(string(received))
   645  		if err != nil {
   646  			t.Error(err)
   647  		}
   648  
   649  		expected, err := createExpectedHTML("admin/event", map[string]any{
   650  			"event": events[0],
   651  		})
   652  		if err != nil {
   653  			t.Error(err)
   654  		}
   655  		expectedMinimized, err := minimizeHTML(expected)
   656  		if err != nil {
   657  			t.Error(err)
   658  		}
   659  
   660  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   661  			t.Errorf("mismatch (-want +got):\n%s", diff)
   662  		}
   663  	})
   664  
   665  	t.Run("update event status", func(t *testing.T) {
   666  		newStatus := "manual_failed"
   667  		events, err := repo.EventsByOwnerGet(ctx, teams[0].ID, 1)
   668  		if err != nil {
   669  			t.Error(err)
   670  		}
   671  		if len(events) == 0 {
   672  			t.Errorf("get event html: no event found for team %v", teams[0].ID)
   673  		}
   674  
   675  		values := url.Values{"status": {newStatus}}
   676  		resp, err := server.Client().PostForm(fmt.Sprintf("%v/admin/event/%v", server.URL, events[0].ID), values)
   677  		if err != nil {
   678  			t.Error(err)
   679  		}
   680  		defer resp.Body.Close()
   681  
   682  		if resp.StatusCode != http.StatusOK {
   683  			t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK)
   684  		}
   685  
   686  		if resp.Header.Get("Content-Type") != htmlContentType {
   687  			t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType)
   688  		}
   689  
   690  		received, err := io.ReadAll(resp.Body)
   691  		if err != nil {
   692  			t.Error(err)
   693  		}
   694  		receivedMinimized, err := minimizeHTML(string(received))
   695  		if err != nil {
   696  			t.Error(err)
   697  		}
   698  
   699  		events, err = repo.EventsByOwnerGet(ctx, teams[0].ID, 1)
   700  		if err != nil {
   701  			t.Error(err)
   702  		}
   703  
   704  		event := events[0]
   705  		if newStatus != event.Status {
   706  			t.Errorf("expected status %v, got %s", newStatus, event.Status)
   707  		}
   708  
   709  		expected, err := createExpectedHTML("admin/event", map[string]any{
   710  			"event": event,
   711  		})
   712  		if err != nil {
   713  			t.Error(err)
   714  		}
   715  		expectedMinimized, err := minimizeHTML(expected)
   716  		if err != nil {
   717  			t.Error(err)
   718  		}
   719  
   720  		if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" {
   721  			t.Errorf("mismatch (-want +got):\n%s", diff)
   722  		}
   723  	})
   724  }
   725  
   726  func prepareAdminTests(ctx context.Context) ([]gensql.Team, error) {
   727  	// teams
   728  	teamA := gensql.Team{
   729  		ID:    "team-a-1234",
   730  		Slug:  "team-a",
   731  		Users: []string{testUser.Email, "user.one@nav.no"},
   732  	}
   733  	err := repo.TeamCreate(ctx, teamA)
   734  	if err != nil {
   735  		return nil, err
   736  	}
   737  	if err := createChart(ctx, teamA.ID, gensql.ChartTypeJupyterhub); err != nil {
   738  		return nil, err
   739  	}
   740  
   741  	teamB := gensql.Team{
   742  		ID:    "team-b-1234",
   743  		Slug:  "team-b",
   744  		Users: []string{testUser.Email, "user.one@nav.no", "user.two@nav.no"},
   745  	}
   746  	err = repo.TeamCreate(ctx, teamB)
   747  	if err != nil {
   748  		return nil, err
   749  	}
   750  	if err := createChart(ctx, teamB.ID, gensql.ChartTypeJupyterhub); err != nil {
   751  		return nil, err
   752  	}
   753  	if err := createChart(ctx, teamB.ID, gensql.ChartTypeAirflow); err != nil {
   754  		return nil, err
   755  	}
   756  
   757  	_, err = db.Exec("DELETE FROM chart_global_values")
   758  	if err != nil {
   759  		return nil, err
   760  	}
   761  
   762  	if err := repo.GlobalChartValueInsert(ctx, "jupytervalue", "value", false, gensql.ChartTypeJupyterhub); err != nil {
   763  		return nil, err
   764  	}
   765  	if err := repo.GlobalChartValueInsert(ctx, "airflowvalue", "value", false, gensql.ChartTypeAirflow); err != nil {
   766  		return nil, err
   767  	}
   768  
   769  	// events
   770  	if err := repo.RegisterCreateTeamEvent(ctx, teamA); err != nil {
   771  		return nil, err
   772  	}
   773  	if err := repo.RegisterCreateTeamEvent(ctx, teamB); err != nil {
   774  		return nil, err
   775  	}
   776  
   777  	return []gensql.Team{teamA, teamB}, nil
   778  }
   779  
   780  func createChart(ctx context.Context, teamID string, chartType gensql.ChartType) error {
   781  	return repo.TeamValueInsert(ctx, chartType, "dummy", "dummy", teamID)
   782  }
   783  
   784  func getSessionCookieFromResponse(resp *http.Response) (*http.Cookie, error) {
   785  	for _, cookie := range resp.Cookies() {
   786  		if cookie.Name == "session" {
   787  			return cookie, nil
   788  		}
   789  	}
   790  
   791  	return nil, errors.New("no session cookie in http response")
   792  }
   793  
   794  func cleanUpAdminTests(ctx context.Context, teams []gensql.Team) error {
   795  	for _, team := range teams {
   796  		if err := repo.TeamDelete(ctx, team.ID); err != nil {
   797  			return err
   798  		}
   799  	}
   800  
   801  	if err := repo.GlobalValueDelete(ctx, "jupytervalue", gensql.ChartTypeJupyterhub); err != nil {
   802  		return err
   803  	}
   804  	if err := repo.GlobalValueDelete(ctx, "airflowvalue", gensql.ChartTypeAirflow); err != nil {
   805  		return err
   806  	}
   807  
   808  	return nil
   809  }
   810  
   811  func getNewEvents(oldEvents, events []gensql.Event) []gensql.Event {
   812  	var new []gensql.Event
   813  	for _, event := range events {
   814  		if !containsEvent(oldEvents, event) {
   815  			new = append(new, event)
   816  		}
   817  	}
   818  
   819  	return new
   820  }
   821  
   822  func containsEvent(events []gensql.Event, event gensql.Event) bool {
   823  	for _, oldEvent := range events {
   824  		if oldEvent.ID == event.ID {
   825  			return true
   826  		}
   827  	}
   828  
   829  	return false
   830  }