-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
503af3b
commit 4d355a5
Showing
5 changed files
with
186 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
version: "3.9" | ||
services: | ||
server: | ||
image: "expense_tracker/server:latest" | ||
build: | ||
context: ./.. | ||
dockerfile: app_to_test/server/Dockerfile | ||
environment: | ||
DB_URL: "postgres://postgres:secret123@db:5432/expense_tracker?sslmode=disable" | ||
BANK_API_URL: "${BANK_API_URL}" | ||
depends_on: | ||
db: | ||
condition: service_healthy | ||
networks: | ||
- default | ||
ports: | ||
# Once again, we use a random port to avoid conflicts. | ||
- "8000" | ||
|
||
db: | ||
image: "postgres:15.2-alpine" | ||
environment: | ||
POSTGRES_DB: expense_tracker | ||
POSTGRES_USER: postgres | ||
POSTGRES_PASSWORD: secret123 | ||
healthcheck: | ||
test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ] | ||
interval: 3s | ||
timeout: 60s | ||
retries: 10 | ||
start_period: 5s | ||
|
||
migrate: | ||
image: "expense_tracker/migrate:latest" | ||
build: | ||
context: ./../app_to_test/db | ||
environment: | ||
DB_URL: "postgres://postgres:secret123@db:5432/expense_tracker?sslmode=disable" | ||
depends_on: | ||
db: | ||
condition: service_healthy | ||
|
||
networks: | ||
default: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
//go:build e2e_tests | ||
|
||
package test_e2e | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/google/uuid" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"github.com/testcontainers/testcontainers-go" | ||
tc "github.com/testcontainers/testcontainers-go/modules/compose" | ||
"github.com/testcontainers/testcontainers-go/wait" | ||
) | ||
|
||
const expenseToSyncID = "677df0c4-d829-42eb-a0c9-29d5b0a2bbe4" | ||
|
||
func TestE2E(t *testing.T) { | ||
ctx := context.Background() | ||
|
||
// At first, spin up mocked bank API, and get its address. | ||
bankAPIAddress := mockBankAPI(t) | ||
address := startApp(t, ctx, bankAPIAddress) | ||
|
||
t.Run("app is starting properly", func(t *testing.T) { | ||
response, err := http.Get(fmt.Sprintf("%s/expenses/all", address)) | ||
|
||
require.NoError(t, err) | ||
assert.Equal(t, http.StatusOK, response.StatusCode, "status code") | ||
}) | ||
t.Run("sync expenses", func(t *testing.T) { | ||
response, err := http.Get(fmt.Sprintf("%s/expenses/sync", address)) | ||
|
||
require.NoError(t, err) | ||
require.Equal(t, http.StatusOK, response.StatusCode, "status code") | ||
|
||
// After sync, our app should now store expense from mocked API. | ||
response, err = http.Get(fmt.Sprintf("%s/expenses/all", address)) | ||
require.NoError(t, err) | ||
responseBody, err := io.ReadAll(response.Body) | ||
require.NoError(t, err) | ||
assert.Contains(t, string(responseBody), expenseToSyncID) | ||
}) | ||
} | ||
|
||
func startApp(t *testing.T, ctx context.Context, bankAPIAddress string) (address string) { | ||
t.Helper() | ||
|
||
compose, err := tc.NewDockerComposeWith( | ||
tc.WithStackFiles("./docker-compose-for-e2e.yaml"), | ||
// Giving unique name to each docker compose stack allows us to run tests in parallel. | ||
tc.StackIdentifier(uuid.New().String()), | ||
) | ||
require.NoError(t, err, "docker compose setup") | ||
|
||
t.Cleanup(func() { | ||
// When test fail, printing logs is usually helpful :) | ||
if t.Failed() { | ||
reader, _ := getServerContainer(t, ctx, compose).Logs(ctx) | ||
bytes, _ := io.ReadAll(reader) | ||
fmt.Println(`\nLogs from "server" container:\n`, string(bytes)) | ||
} | ||
assert.NoError(t, compose.Down(ctx, tc.RemoveOrphans(true), tc.RemoveImagesLocal)) | ||
}) | ||
|
||
err = compose. | ||
WithEnv(map[string]string{ | ||
"BANK_API_URL": bankAPIAddress, | ||
}). | ||
WaitForService("server", wait.ForLog("running...")). | ||
Up(ctx) | ||
require.NoError(t, err, "docker compose up") | ||
|
||
// Port is randomly assigned by docker. We need to get it. | ||
apiPort, err := getServerContainer(t, ctx, compose).MappedPort(ctx, "8000") | ||
require.NoError(t, err, "docker compose server port") | ||
|
||
return fmt.Sprintf("http://localhost:%s", apiPort.Port()) | ||
} | ||
|
||
func getServerContainer(t *testing.T, ctx context.Context, compose tc.ComposeStack) testcontainers.Container { | ||
t.Helper() | ||
|
||
serverContainer, err := compose.ServiceContainer(ctx, "server") | ||
require.NoError(t, err, "docker compose server container") | ||
|
||
return serverContainer | ||
} | ||
|
||
func mockBankAPI(t *testing.T) (address string) { | ||
t.Helper() | ||
|
||
// We are getting random available port. | ||
// Using 0.0.0.0 address if preferable over localhost, as the former will usually not work in CI. | ||
// Oh, and Desktop Docker doesn't support IPv6 yet, so it's better to specify "tcp4" network. | ||
listener, err := net.Listen("tcp4", "0.0.0.0:0") | ||
require.NoError(t, err, "could not start listener") | ||
addr, err := net.ResolveTCPAddr(listener.Addr().Network(), listener.Addr().String()) | ||
require.NoError(t, err, "could not resolve tcp addr") | ||
|
||
// Setup mocked API. | ||
mux := http.NewServeMux() | ||
mux.Handle("/get-transactions", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Write([]byte(fmt.Sprintf(` | ||
[ | ||
{ | ||
"id": "%s", | ||
"amount": 500.00, | ||
"category": "food", | ||
"created_at": "2020-01-01T00:00:00Z" | ||
} | ||
]`, | ||
expenseToSyncID))) | ||
})) | ||
|
||
server := httptest.NewUnstartedServer(mux) | ||
server.Listener = listener | ||
server.Start() | ||
t.Cleanup(func() { | ||
server.Close() | ||
}) | ||
|
||
// "host.docker.internal" resolves to host network. Thanks to this we can access our HTTP mocks from container. | ||
return fmt.Sprintf("http://host.docker.internal:%d", addr.Port) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters