Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TEST_CLICKHOUSE_URL="localhost:39000"
TEST_LOCALSTACK_URL="localhost:34566"
TEST_RABBITMQ_URL="localhost:35672"
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ down/uptrace:
up/portal:
cd internal/portal && npm install && npm run dev

up/test:
docker-compose -f build/test/compose.yml up -d

down/test:
docker-compose -f build/test/compose.yml down

test:
go test $(TEST) $(TESTARGS)

Expand Down
19 changes: 19 additions & 0 deletions build/test/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: "outpost-test"

services:
clickhouse:
image: clickhouse/clickhouse-server:24-alpine
ports:
- 39000:9000
rabbitmq:
image: rabbitmq:3-management
ports:
- 35672:5672
- 45672:15672
aws:
image: localstack/localstack:latest
environment:
- SERVICES=sns,sts,sqs
ports:
- 34566:4566
- 34571:4571
169 changes: 169 additions & 0 deletions cmd/e2e/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,146 @@ func (suite *basicSuite) TestDestinationsListAPI() {
suite.RunAPITests(suite.T(), tests)
}

func (suite *basicSuite) TestDestinationEnableDisableAPI() {
tenantID := uuid.New().String()
sampleDestinationID := uuid.New().String()
tests := []APITest{
{
Name: "PUT /:tenantID",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPUT,
Path: "/" + tenantID,
}),
Expected: APITestExpectation{
Match: &httpclient.Response{
StatusCode: http.StatusCreated,
},
},
},
{
Name: "POST /:tenantID/destinations",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPOST,
Path: "/" + tenantID + "/destinations",
Body: map[string]interface{}{
"id": sampleDestinationID,
"type": "webhook",
"topics": "*",
"config": map[string]interface{}{
"url": "http://host.docker.internal:4444",
},
},
}),
Expected: APITestExpectation{
Match: &httpclient.Response{
StatusCode: http.StatusCreated,
},
},
},
{
Name: "GET /:tenantID/destinations/:destinationID",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodGET,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID,
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, false),
},
},
{
Name: "PUT /:tenantID/destinations/:destinationID/disable",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPUT,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID + "/disable",
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, true),
},
},
{
Name: "GET /:tenantID/destinations/:destinationID",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodGET,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID,
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, true),
},
},
{
Name: "PUT /:tenantID/destinations/:destinationID/enable",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPUT,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID + "/enable",
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, false),
},
},
{
Name: "GET /:tenantID/destinations/:destinationID",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodGET,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID,
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, false),
},
},
{
Name: "PUT /:tenantID/destinations/:destinationID/enable duplicate",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPUT,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID + "/enable",
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, false),
},
},
{
Name: "GET /:tenantID/destinations/:destinationID",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodGET,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID,
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, false),
},
},
{
Name: "PUT /:tenantID/destinations/:destinationID/disable",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPUT,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID + "/disable",
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, true),
},
},
{
Name: "PUT /:tenantID/destinations/:destinationID/disable duplicate",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodPUT,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID + "/disable",
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, true),
},
},
{
Name: "GET /:tenantID/destinations/:destinationID",
Request: suite.AuthRequest(httpclient.Request{
Method: httpclient.MethodGET,
Path: "/" + tenantID + "/destinations/" + sampleDestinationID,
}),
Expected: APITestExpectation{
Validate: makeDestinationDisabledValidator(sampleDestinationID, true),
},
},
}
suite.RunAPITests(suite.T(), tests)
}

func makeDestinationListValidator(length int) map[string]any {
return map[string]any{
"type": "object",
Expand Down Expand Up @@ -767,3 +907,32 @@ func makeDestinationListValidator(length int) map[string]any {
},
}
}

func makeDestinationDisabledValidator(id string, disabled bool) map[string]any {
var disabledValidator map[string]any
if disabled {
disabledValidator = map[string]any{
"type": "string",
"minLength": 1,
}
} else {
disabledValidator = map[string]any{
"type": "null",
}
}
return map[string]interface{}{
"properties": map[string]interface{}{
"statusCode": map[string]interface{}{
"const": 200,
},
"body": map[string]interface{}{
"properties": map[string]interface{}{
"id": map[string]interface{}{
"const": id,
},
"disabled_at": disabledValidator,
},
},
},
}
}
31 changes: 8 additions & 23 deletions cmd/e2e/configs/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package configs
import (
"testing"

"github.com/hookdeck/outpost/internal/clickhouse"
"github.com/hookdeck/outpost/internal/config"
"github.com/hookdeck/outpost/internal/mqs"
"github.com/hookdeck/outpost/internal/util/testinfra"
"github.com/hookdeck/outpost/internal/util/testutil"
)

Expand All @@ -17,27 +16,13 @@ func Basic(t *testing.T) (*config.Config, func(), error) {
}
}

// Testcontainer
chEndpoint, cleanupCH, err := testutil.StartTestContainerClickHouse()
if err != nil {
return nil, cleanup, err
}
cleanupFns = append(cleanupFns, cleanupCH)

awsEndpoint, cleanupAWS, err := testutil.StartTestcontainerLocalstack()
if err != nil {
return nil, cleanup, err
}
cleanupFns = append(cleanupFns, cleanupAWS)
t.Cleanup(testinfra.Start(t))

// Config
redisConfig := testutil.CreateTestRedisConfig(t)
clickHouseConfig := &clickhouse.ClickHouseConfig{
Addr: chEndpoint,
Username: "default",
Password: "",
Database: "default",
}
clickHouseConfig := testinfra.NewClickHouseConfig(t)
deliveryMQConfig := testinfra.NewMQAWSConfig(t, nil)
logMQConfig := testinfra.NewMQAWSConfig(t, nil)

return &config.Config{
Hostname: "outpost",
Expand All @@ -49,11 +34,11 @@ func Basic(t *testing.T) (*config.Config, func(), error) {
PortalProxyURL: "",
Topics: testutil.TestTopics,
Redis: redisConfig,
ClickHouse: clickHouseConfig,
ClickHouse: &clickHouseConfig,
OpenTelemetry: nil,
PublishQueueConfig: nil,
DeliveryQueueConfig: &mqs.QueueConfig{AWSSQS: &mqs.AWSSQSConfig{Endpoint: awsEndpoint, Region: "us-east-1", ServiceAccountCredentials: "test:test:", Topic: "delivery"}},
LogQueueConfig: &mqs.QueueConfig{AWSSQS: &mqs.AWSSQSConfig{Endpoint: awsEndpoint, Region: "us-east-1", ServiceAccountCredentials: "test:test:", Topic: "log"}},
DeliveryQueueConfig: &deliveryMQConfig,
LogQueueConfig: &logMQConfig,
PublishMaxConcurrency: 3,
DeliveryMaxConcurrency: 3,
LogMaxConcurrency: 3,
Expand Down
46 changes: 45 additions & 1 deletion docs/contributing/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ $ TESTARGS='-v -run "TestJWT"'' make test
# go test $(go list ./...) -v -run "TestJWT"
```

Keep in mind you can't use `-run "Test..."` along with `make test/integration` as the integration test already specify integration tests with `-run` option. However, since you're already specifying which test to run, I assume this is a non-issue.
Keep in mind you can't use `-run "Test..."` along with `make test/integration` as the integration test already specify integration tests with `-run` option. However, since you're already specifying which test to run, we assume this is a non-issue.

## Coverage

Expand All @@ -62,3 +62,47 @@ Running the coverage test command above will generate the `coverage.out` file. Y
$ make test/coverage/html
# go tool cover -html=coverage.out
```

## Integration & E2E Tests

When running integration & e2e tests, we often times require some test infrastructure such as ClickHouse, LocalStack, RabbitMQ, etc. We use [Testcontainers](https://testcontainers.com/) for that. It usually takes a few seconds (10s or so) to spawn the necessary containers. To improve the feedback loop, you can run a persistent test infrastructure and skip spawning testcontainers.

To run the test infrastructure:

```sh
$ make up/test

## to take the test infra down
# $ make down/test
```

It will run a Docker compose stack called `outpost-test` which runs the necessary services at ports ":30000 + port". For example, ClickHouse usually runs on port `:9000`, so in the test infra it will run on port `:39000`.

From here, you can provide env variable `TESTINFRA=1` to tell the test suite to use these services instead of spawning testcontainers.

```sh
$ TESTINFRA=1 make test
```

Tip: You can `$ export TESTINFRA=1` to use the test infra for the whole terminal session.

### Integration Test Template

Here's a short template for how you can write integration tests that require an external test infra:

```golang
// Integration test should always start with "TestIntegration...() {}"
func TestIntegrationMyIntegrationTest(t *testing.T) {
t.Parallel()

// call testinfra.Start(t) to signal that you require the test infra.
// This helps the test runner properly terminate resources at the end.
t.Cleanup(testinfra.Start(t))

// use whichever infra you need
chConfig := testinfra.NewClickHouseConfig(t)
awsMQConfig := testinfra.NewMQAWSConfig(t, attributesMap)
rabbitmqConfig := testinfra.NewMQRabbitMQConfig(t)
// ...
}
```
Loading