From 5df30b9eb0890403c8904e9abc186b5dbc426d5b Mon Sep 17 00:00:00 2001 From: matias-portainer <104775949+matias-portainer@users.noreply.github.com> Date: Tue, 10 May 2022 11:58:19 -0300 Subject: [PATCH] chore(edge): add unit tests to edgestatus inspect endpoint EE-3088 (#6905) * chore(edge): add unit tests to edgestatus inspect endpoint EE-3088 --- .../endpoint_edgestatus_inspect_test.go | 445 ++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 api/http/handler/endpointedge/endpoint_edgestatus_inspect_test.go diff --git a/api/http/handler/endpointedge/endpoint_edgestatus_inspect_test.go b/api/http/handler/endpointedge/endpoint_edgestatus_inspect_test.go new file mode 100644 index 000000000..f13b2d2e5 --- /dev/null +++ b/api/http/handler/endpointedge/endpoint_edgestatus_inspect_test.go @@ -0,0 +1,445 @@ +package endpointedge + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/apikey" + "github.com/portainer/portainer/api/chisel" + "github.com/portainer/portainer/api/datastore" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/jwt" + + "github.com/stretchr/testify/assert" +) + +type endpointTestCase struct { + endpoint portainer.Endpoint + endpointRelation portainer.EndpointRelation + expectedStatusCode int +} + +var endpointTestCases = []endpointTestCase{ + { + portainer.Endpoint{}, + portainer.EndpointRelation{}, + http.StatusNotFound, + }, + { + portainer.Endpoint{ + ID: -1, + Name: "endpoint-id--1", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "edge-id", + }, + portainer.EndpointRelation{ + EndpointID: -1, + }, + http.StatusNotFound, + }, + { + portainer.Endpoint{ + ID: 2, + Name: "endpoint-id-2", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "", + }, + portainer.EndpointRelation{ + EndpointID: 2, + }, + http.StatusBadRequest, + }, + { + portainer.Endpoint{ + ID: 4, + Name: "endpoint-id-4", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "edge-id", + }, + portainer.EndpointRelation{ + EndpointID: 4, + }, + http.StatusOK, + }, +} + +func setupHandler() (*Handler, func(), error) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test") + if err != nil { + return nil, nil, fmt.Errorf("could not create a tmp dir: %w", err) + } + + fs, err := filesystem.NewService(tmpDir, "") + if err != nil { + return nil, nil, fmt.Errorf("could not start a new filesystem service: %w", err) + } + + _, store, storeTeardown := datastore.MustNewTestStore(true, true) + + ctx := context.Background() + shutdownCtx, cancelFn := context.WithCancel(ctx) + + teardown := func() { + cancelFn() + storeTeardown() + } + + jwtService, err := jwt.NewService("1h", store) + if err != nil { + teardown() + return nil, nil, fmt.Errorf("could not start a new jwt service: %w", err) + } + + apiKeyService := apikey.NewAPIKeyService(nil, nil) + + settings, err := store.Settings().Settings() + if err != nil { + teardown() + return nil, nil, fmt.Errorf("could not create new settings: %w", err) + } + settings.TrustOnFirstConnect = true + + err = store.Settings().UpdateSettings(settings) + if err != nil { + teardown() + return nil, nil, fmt.Errorf("could not update settings: %w", err) + } + + handler := NewHandler( + security.NewRequestBouncer(store, jwtService, apiKeyService), + store, + fs, + chisel.NewService(store, shutdownCtx), + ) + + handler.ReverseTunnelService = chisel.NewService(store, shutdownCtx) + + return handler, teardown, nil +} + +func createEndpoint(handler *Handler, endpoint portainer.Endpoint, endpointRelation portainer.EndpointRelation) (err error) { + // Avoid setting ID below 0 to generate invalid test cases + if endpoint.ID <= 0 { + return nil + } + + err = handler.DataStore.Endpoint().Create(&endpoint) + if err != nil { + return err + } + + return handler.DataStore.EndpointRelation().Create(&endpointRelation) +} + +func TestMissingEdgeIdentifier(t *testing.T) { + handler, teardown, err := setupHandler() + defer teardown() + + if err != nil { + t.Fatal(err) + } + + endpointID := portainer.EndpointID(45) + err = createEndpoint(handler, portainer.Endpoint{ + ID: endpointID, + Name: "endpoint-id-45", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "edge-id", + }, portainer.EndpointRelation{EndpointID: endpointID}) + + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil) + if err != nil { + t.Fatal("request error:", err) + } + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusForbidden { + t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code)) + } +} + +func TestWithEndpoints(t *testing.T) { + handler, teardown, err := setupHandler() + defer teardown() + + if err != nil { + t.Fatal(err) + } + + for _, test := range endpointTestCases { + err = createEndpoint(handler, test.endpoint, test.endpointRelation) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil) + if err != nil { + t.Fatal("request error:", err) + } + req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id") + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != test.expectedStatusCode { + t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID)) + } + } +} + +func TestLastCheckInDateIncreases(t *testing.T) { + handler, teardown, err := setupHandler() + defer teardown() + + if err != nil { + t.Fatal(err) + } + + endpointID := portainer.EndpointID(56) + endpoint := portainer.Endpoint{ + ID: endpointID, + Name: "test-endpoint-56", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "edge-id", + LastCheckInDate: time.Now().Unix(), + } + + endpointRelation := portainer.EndpointRelation{ + EndpointID: endpoint.ID, + } + + err = createEndpoint(handler, endpoint, endpointRelation) + if err != nil { + t.Fatal(err) + } + + time.Sleep(1 * time.Second) + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil) + if err != nil { + t.Fatal("request error:", err) + } + req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id") + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code)) + } + + updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) + if err != nil { + t.Fatal(err) + } + + assert.Greater(t, updatedEndpoint.LastCheckInDate, endpoint.LastCheckInDate) +} + +func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) { + handler, teardown, err := setupHandler() + defer teardown() + + if err != nil { + t.Fatal(err) + } + + endpointID := portainer.EndpointID(44) + edgeId := "edge-id" + endpoint := portainer.Endpoint{ + ID: endpointID, + Name: "test-endpoint-44", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "", + } + endpointRelation := portainer.EndpointRelation{ + EndpointID: endpoint.ID, + } + + err = createEndpoint(handler, endpoint, endpointRelation) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil) + if err != nil { + t.Fatal("request error:", err) + } + req.Header.Set(portainer.PortainerAgentEdgeIDHeader, edgeId) + req.Header.Set(portainer.HTTPResponseAgentPlatform, "1") + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code)) + } + + updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, updatedEndpoint.EdgeID, edgeId) +} + +func TestEdgeStackStatus(t *testing.T) { + handler, teardown, err := setupHandler() + defer teardown() + + if err != nil { + t.Fatal(err) + } + + endpointID := portainer.EndpointID(7) + endpoint := portainer.Endpoint{ + ID: endpointID, + Name: "test-endpoint-7", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "edge-id", + LastCheckInDate: time.Now().Unix(), + } + + edgeStackID := portainer.EdgeStackID(17) + edgeStack := portainer.EdgeStack{ + ID: edgeStackID, + Name: "test-edge-stack-17", + Status: map[portainer.EndpointID]portainer.EdgeStackStatus{ + endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpoint.ID}, + }, + CreationDate: time.Now().Unix(), + EdgeGroups: []portainer.EdgeGroupID{1, 2}, + ProjectPath: "/project/path", + EntryPoint: "entrypoint", + Version: 237, + ManifestPath: "/manifest/path", + DeploymentType: 1, + } + + endpointRelation := portainer.EndpointRelation{ + EndpointID: endpoint.ID, + EdgeStacks: map[portainer.EdgeStackID]bool{ + edgeStack.ID: true, + }, + } + handler.DataStore.EdgeStack().Create(edgeStack.ID, &edgeStack) + + err = createEndpoint(handler, endpoint, endpointRelation) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil) + if err != nil { + t.Fatal("request error:", err) + } + req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id") + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code)) + } + + var data endpointEdgeStatusInspectResponse + err = json.NewDecoder(rec.Body).Decode(&data) + if err != nil { + t.Fatal("error decoding response:", err) + } + + assert.Len(t, data.Stacks, 1) + assert.Equal(t, edgeStack.ID, data.Stacks[0].ID) + assert.Equal(t, edgeStack.Version, data.Stacks[0].Version) +} + +func TestEdgeJobsResponse(t *testing.T) { + handler, teardown, err := setupHandler() + defer teardown() + + if err != nil { + t.Fatal(err) + } + + endpointID := portainer.EndpointID(77) + endpoint := portainer.Endpoint{ + ID: endpointID, + Name: "test-endpoint-77", + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "https://portainer.io:9443", + EdgeID: "edge-id", + LastCheckInDate: time.Now().Unix(), + } + + endpointRelation := portainer.EndpointRelation{ + EndpointID: endpoint.ID, + } + + err = createEndpoint(handler, endpoint, endpointRelation) + if err != nil { + t.Fatal(err) + } + + path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd")) + if err != nil { + t.Fatal(err) + } + + edgeJobID := portainer.EdgeJobID(35) + edgeJob := portainer.EdgeJob{ + ID: edgeJobID, + Created: time.Now().Unix(), + CronExpression: "* * * * *", + Name: "test-edge-job", + ScriptPath: path, + Recurring: true, + Version: 57, + } + + handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob) + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil) + if err != nil { + t.Fatal("request error:", err) + } + req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id") + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code)) + } + + var data endpointEdgeStatusInspectResponse + err = json.NewDecoder(rec.Body).Decode(&data) + if err != nil { + t.Fatal("error decoding response:", err) + } + + assert.Len(t, data.Schedules, 1) + assert.Equal(t, edgeJob.ID, data.Schedules[0].ID) + assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression) + assert.Equal(t, edgeJob.Version, data.Schedules[0].Version) +}