Skip to content

Commit

Permalink
Create a mock fleet server for integration tests (#2695)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndersonQ authored Jun 2, 2023
1 parent fdc46bf commit 50428cd
Show file tree
Hide file tree
Showing 7 changed files with 1,121 additions and 0 deletions.
42 changes: 42 additions & 0 deletions testing/fleetservertest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Mock Fleet Server

It's mock for fleet-server allowing to test the Agent interactions with
fleet-server without the need of running a fleet-server and having full
control of it to test even edge cases such as error handling.

## tl;dr

- See [`fleetservertest_test.go`](fleetserver_test.go) for examples.

- `fleetservertest.API` defines a `handlernameFn` property for each available handlers. By default, any not implemented handler returns a `http.StatusNotImplemented`.

- Use `fleetservertest.NewServer(fleetservertest.API{})` to create a new test server. It's a `*httptest.Server`:

```go
NewServer(API{
AckFn: nil,
CheckinFn: nil,
EnrollFn: nil,
ArtifactFn: nil,
StatusFn: nil,
UploadBeginFn: nil,
UploadChunkFn: nil,
UploadCompleteFn: nil,
})
```

- Use the `fleetservertest.NewPATHNAME(args)` functions to get a path ready to be used:
```go
p := NewPathAgentAcks("my-agent-id")
// p = "/api/fleet/agents/my-agent-id/acks"
```

- Use `fleetservertest.NewHANDERNAME()` to get a ready to use handler:
```go
ts := fleetservertest.NewServer(API{
CheckinFn: fleetservertest.NewCheckinHandler("agentID", "ackToken", false),
})
```

- Check [`handlers.go`](handlers.go) for the available paths and handlers.
- Check [`models.go`](models.go) for the request and response models or the [openapi](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/elastic/fleet-server/main/model/openapi.yml#/) definition.
7 changes: 7 additions & 0 deletions testing/fleetservertest/checkin.go

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions testing/fleetservertest/fleetserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package fleetservertest

import (
"context"
"io"
"net/http/httptest"
)

// API holds the handlers for the fleet-api, see https://petstore.swagger.io/?url=https://raw.githubusercontent.com/elastic/fleet-server/main/model/openapi.yml
// for rendered OpenAPI definition. If any of the handlers are nil, a
// http.StatusNotImplemented is returned for the route.
type API struct {
AckFn func(
ctx context.Context,
id string,
ackRequest AckRequest) (*AckResponse, *HTTPError)

CheckinFn func(
ctx context.Context,
id string,
userAgent string,
acceptEncoding string,
checkinRequest CheckinRequest) (*CheckinResponse, *HTTPError)

EnrollFn func(
ctx context.Context,
id string,
userAgent string,
enrollRequest EnrollRequest) (*EnrollResponse, *HTTPError)

ArtifactFn func(
ctx context.Context,
id string,
sha2 string) *HTTPError

StatusFn func(
ctx context.Context) (*StatusResponse, *HTTPError)

UploadBeginFn func(
ctx context.Context,
requestBody UploadBeginRequest) (*UploadBeginResponse, *HTTPError)

UploadChunkFn func(
ctx context.Context,
id string,
chunkNum int32,
xChunkSHA2 string,
body io.ReadCloser) *HTTPError

UploadCompleteFn func(
ctx context.Context,
id string,
uploadCompleteRequest UploadCompleteRequest) *HTTPError
}

// NewServer returns a new started *httptest.Server mocking the Fleet Server API.
// If a route is called and its handler (the *Fn field) is nil a.
// http.StatusNotImplemented error will be returned.
func NewServer(api API) *httptest.Server {
mux := NewRouter(Handlers{api: api})

return httptest.NewServer(mux)
}
84 changes: 84 additions & 0 deletions testing/fleetservertest/fleetserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package fleetservertest

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
)

func ExampleNewServer_status() {
ts := NewServer(API{
StatusFn: NewStatusHandlerHealth(),
})

resp, err := http.Get(ts.URL + PathStatus) //nolint:noctx // it's fine on a test
if err != nil {
panic(fmt.Sprintf("could not make request to fleet-test-server: %v", err))
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
if err != nil {
panic(fmt.Sprintf("could not read response: %v", err))
}
}
fmt.Printf("%s", body)

// Output:
// {"name":"fleet-server","status":"HEALTHY"}
}

func ExampleNewServer_checkin() {
agentID := "agentID"

ts := NewServer(API{
CheckinFn: NewCheckinHandler(agentID, "", false),
})

cmd := fleetapi.NewCheckinCmd(
agentInfo(agentID), sender{url: ts.URL, path: NewPathCheckin(agentID)})
resp, _, err := cmd.Execute(context.Background(), &fleetapi.CheckinRequest{})
if err != nil {
panic(fmt.Sprintf("failed executing checkin: %v", err))
}

fmt.Println(resp.Actions)

// Output:
// [action_id: policy:24e4d030-ffa7-11ed-b040-9debaa5fecb8:2:1, type: POLICY_CHANGE]
}

type agentInfo string

func (a agentInfo) AgentID() string {
return ""
}

type sender struct {
url, path string
}

func (s sender) Send(
ctx context.Context,
method string,
path string,
params url.Values,
headers http.Header,
body io.Reader) (*http.Response, error) {
return &http.Response{
Status: http.StatusText(http.StatusOK),
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(
checkinResponseJSONPolicySystemIntegration)),
}, nil
}

func (s sender) URI() string {
return s.url + s.path
}
113 changes: 113 additions & 0 deletions testing/fleetservertest/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package fleetservertest

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
)

const (
PathAgentAcks = "/api/fleet/agents/{id}/acks"
PathAgentCheckin = "/api/fleet/agents/{id}/checkin"
PathAgentEnroll = "/api/fleet/agents/{id}"

PathArtifact = "/api/fleet/artifacts/{id}/{sha2}"
PathStatus = "/api/status"

PathUploadBegin = "/api/fleet/uploads"
PathUploadChunk = "/api/fleet/uploads/{id}/{chunkNum}"
PathUploadComplete = "/api/fleet/uploads/{id}"
)

func NewPathAgentAcks(agentID string) string {
return strings.Replace(PathAgentAcks, "{id}", agentID, 1)
}

func NewPathCheckin(agentID string) string {
return strings.Replace(PathAgentCheckin, "{id}", agentID, 1)
}

func NewPathAgentEnroll(agentID string) string {
return strings.Replace(PathAgentEnroll, "{id}", agentID, 1)
}

func NewPathArtifact(agentID, sha2 string) string {
return strings.Replace(
strings.Replace(PathArtifact, "{id}", agentID, 1),
"{sha2}",
sha2,
1)
}

func NewPathUploadBegin() string {
return PathUploadBegin
}

func NewPathUploadChunk(agentID, chunkNum string) string {
return strings.Replace(
strings.Replace(PathUploadChunk, "{id}", agentID, 1),
"{chunkNum}",
chunkNum,
1)
}

func NewPathUploadComplete(agentID string) string {
return strings.Replace(PathUploadComplete, "{id}", agentID, 1)
}

func NewCheckinHandler(agentID, ackToken string, withEndpoint bool) func(
ctx context.Context,
id string,
userAgent string,
acceptEncoding string,
checkinRequest CheckinRequest) (*CheckinResponse, *HTTPError) {

policy := checkinResponseJSONPolicySystemIntegration
if withEndpoint {
policy = checkinResponseJSONPolicySystemIntegrationAndEndpoint
}

return func(
ctx context.Context,
id string,
userAgent string,
acceptEncoding string,
checkinRequest CheckinRequest) (*CheckinResponse, *HTTPError) {

resp := CheckinResponse{}
err := json.Unmarshal(
[]byte(fmt.Sprintf(policy, ackToken, agentID)),
&resp)
if err != nil {
return nil, &HTTPError{
StatusCode: http.StatusInternalServerError,
Error: err.Error(),
Message: "failed to unmarshal policy",
}
}

return &resp, nil
}
}

func NewStatusHandlerHealth() func(ctx context.Context) (*StatusResponse, *HTTPError) {
return func(ctx context.Context) (*StatusResponse, *HTTPError) {
return &StatusResponse{
Name: "fleet-server",
Status: "HEALTHY",
// fleet-server does not respond with version information
}, nil
}
}

func NewStatusHandlerUnhealth() func(ctx context.Context) (*StatusResponse, *HTTPError) {
return func(ctx context.Context) (*StatusResponse, *HTTPError) {
return &StatusResponse{
Name: "fleet-server",
Status: "UNHEALTHY",
// fleet-server does not respond with version information
}, &HTTPError{StatusCode: http.StatusInternalServerError}
}
}
Loading

0 comments on commit 50428cd

Please sign in to comment.