diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 41d71ca..abe44c1 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -59,12 +59,14 @@ jobs: run: | sudo apt-get update sudo apt-get install --yes postgresql-client + - name: Create database user + run: | + psql --host localhost --username postgres < databases/create_databases.sql - name: Run integration tests env: - PGHOST: localhost - PGUSER: postgres OPEN_AI_KEY: ${{ secrets.OPEN_AI_KEY }} - run: ./test-integration.sh + run: | + go test ./cmd/integrationtest -tags=integration build-app: runs-on: ubuntu-latest needs: [ test, integration-test ] diff --git a/README.md b/README.md index 357170d..1c74b33 100644 --- a/README.md +++ b/README.md @@ -120,5 +120,5 @@ The integration test script runs the collector and analyzer, then tests the app ```shell source .env -./test-integration.sh +go test -count=1 ./cmd/integrationtest -tags=integration ``` diff --git a/cmd/integrationtest/app_test.go b/cmd/integrationtest/app_test.go new file mode 100644 index 0000000..6ee6710 --- /dev/null +++ b/cmd/integrationtest/app_test.go @@ -0,0 +1,125 @@ +//go:build integration + +package integration_test + +import ( + "context" + "database/sql" + "fmt" + "github.com/initialcapacity/ai-starter/pkg/dbsupport" + "github.com/initialcapacity/ai-starter/pkg/testsupport" + "github.com/initialcapacity/ai-starter/pkg/websupport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "io" + "net/http" + "os" + "os/exec" + "strings" + "testing" +) + +func TestIntegration(t *testing.T) { + client := http.Client{} + openAiKey := websupport.RequireEnvironmentVariable[string]("OPEN_AI_KEY") + dbUrl := "postgres://starter:starter@localhost:5432/starter_integration?sslmode=disable" + + createIntegrationDatabase(t) + prepareIntegrationDatabase(t) + prepareBuildDirectory(t) + + build(t, "migrate") + build(t, "cannedrss") + build(t, "collector") + build(t, "analyzer") + build(t, "app") + + testCtx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + startCommand(t, testCtx, "./build/cannedrss") + startCommand(t, testCtx, "./build/app", + fmt.Sprintf("DATABASE_URL=%s", dbUrl), + fmt.Sprintf("OPEN_AI_KEY=%s", openAiKey), + "HOST=localhost", + "PORT=8234") + + runCommand(t, testCtx, "./build/migrate", + fmt.Sprintf("DATABASE_URL=%s", dbUrl), + "MIGRATIONS_LOCATION=file://../../databases/starter") + runCommand(t, testCtx, "./build/collector", + fmt.Sprintf("DATABASE_URL=%s", dbUrl), + "FEEDS=http://localhost:8123") + runCommand(t, testCtx, "./build/analyzer", + fmt.Sprintf("DATABASE_URL=%s", dbUrl), + fmt.Sprintf("OPEN_AI_KEY=%s", openAiKey)) + + getResponse, err := client.Get("http://localhost:8234/") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, getResponse.StatusCode) + getBody := readBody(t, getResponse) + assert.Contains(t, getBody, "What would you like to know") + + postResponse, err := client.Post("http://localhost:8234/", "application/x-www-form-urlencoded", strings.NewReader("query=tell%20me%20about%20pickles")) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, postResponse.StatusCode) + postBody := readBody(t, postResponse) + assert.Contains(t, postBody, "http://localhost:8123/pickles") + assert.Contains(t, postBody, "") +} + +func createIntegrationDatabase(t *testing.T) { + testsupport.WithSuperDb(t, func(superDb *sql.DB) { + execute(t, superDb, "drop database if exists starter_integration") + execute(t, superDb, "create database starter_integration") + execute(t, superDb, "grant all privileges on database starter_integration to starter") + }) +} + +func prepareIntegrationDatabase(t *testing.T) { + integrationDb := dbsupport.CreateConnection("postgres://super_test@localhost:5432/starter_integration?sslmode=disable") + defer func() { + err := integrationDb.Close() + require.NoError(t, err) + }() + + execute(t, integrationDb, "create extension if not exists vector") + execute(t, integrationDb, "grant usage, create on schema public to starter") +} + +func execute(t *testing.T, db *sql.DB, command string) { + _, err := db.Exec(command) + require.NoError(t, err, fmt.Sprintf("unable to execute %s", command)) +} + +func prepareBuildDirectory(t *testing.T) { + err := os.RemoveAll("../../build") + require.NoError(t, err) + err = os.MkdirAll("./build", os.ModePerm) + require.NoError(t, err) +} + +func build(t *testing.T, name string) { + err := exec.Command("go", "build", "-o", fmt.Sprintf("./build/%s", name), fmt.Sprintf("../../cmd/%s", name)).Run() + require.NoError(t, err, fmt.Sprintf("build %s failed", name)) +} + +func startCommand(t *testing.T, ctx context.Context, command string, environment ...string) { + cmd := exec.CommandContext(ctx, command) + cmd.Env = append(cmd.Env, environment...) + err := cmd.Start() + require.NoError(t, err, fmt.Sprintf("failed to start %s", command)) +} + +func runCommand(t *testing.T, ctx context.Context, command string, environment ...string) { + cmd := exec.CommandContext(ctx, command) + cmd.Env = append(cmd.Env, environment...) + err := cmd.Run() + require.NoError(t, err, fmt.Sprintf("failed to run %s", command)) +} + +func readBody(t *testing.T, response *http.Response) string { + body, err := io.ReadAll(response.Body) + require.NoError(t, err, "unable to read response body") + return string(body) +} diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go index e6d117e..6adf3ca 100644 --- a/cmd/migrate/main.go +++ b/cmd/migrate/main.go @@ -11,7 +11,8 @@ import ( func main() { databaseUrl := websupport.RequireEnvironmentVariable[string]("DATABASE_URL") - migration, err := migrate.New("file://./databases/starter", databaseUrl) + migrationsLocation := websupport.EnvironmentVariable("MIGRATIONS_LOCATION", "file://./databases/starter") + migration, err := migrate.New(migrationsLocation, databaseUrl) if err != nil { log.Fatalf("failed to connect to %s, %s", databaseUrl, err) } diff --git a/databases/create_databases.sql b/databases/create_databases.sql index 9903dad..60f0753 100644 --- a/databases/create_databases.sql +++ b/databases/create_databases.sql @@ -1,4 +1,3 @@ -drop database if exists starter_integration; drop database if exists starter_test; drop database if exists starter_development; drop user starter; @@ -6,12 +5,10 @@ drop user super_test; create database starter_development; create database starter_test; -create database starter_integration; create user starter with password 'starter'; create user super_test superuser ; grant all privileges on database starter_development to starter; grant all privileges on database starter_test to starter; -grant all privileges on database starter_integration to starter; \connect starter_development create extension if not exists vector; @@ -20,7 +17,3 @@ grant usage, create on schema public to starter; \connect starter_test create extension if not exists vector; grant usage, create on schema public to starter; - -\connect starter_integration -create extension if not exists vector; -grant usage, create on schema public to starter; diff --git a/pkg/testsupport/test_connection.go b/pkg/testsupport/test_connection.go index a8c40e9..3b2f144 100644 --- a/pkg/testsupport/test_connection.go +++ b/pkg/testsupport/test_connection.go @@ -17,7 +17,7 @@ type TestDb struct { func NewTestDb(t *testing.T) *TestDb { testDbName := fmt.Sprintf("starter_test_%d", rand.IntN(1_000_000)) - withSuperDb(t, func(superDb *sql.DB) { + WithSuperDb(t, func(superDb *sql.DB) { _, err := superDb.Exec(fmt.Sprintf("create database %s template starter_test", testDbName)) assert.NoError(t, err, "unable to create test database") }) @@ -35,7 +35,7 @@ func (tdb *TestDb) Close() { err := tdb.DB.Close() assert.NoError(tdb.t, err) - withSuperDb(tdb.t, func(superDb *sql.DB) { + WithSuperDb(tdb.t, func(superDb *sql.DB) { _, err = superDb.Exec(fmt.Sprintf("drop database %s", tdb.testDbName)) assert.NoError(tdb.t, err, "unable to drop test database") }) @@ -46,7 +46,7 @@ func (tdb *TestDb) Execute(statement string, arguments ...any) { assert.NoError(tdb.t, err) } -func withSuperDb(t *testing.T, action func(superDb *sql.DB)) { +func WithSuperDb(t *testing.T, action func(superDb *sql.DB)) { superDb := dbsupport.CreateConnection("postgres://super_test@localhost:5432/postgres?sslmode=disable") defer func(superDb *sql.DB) { err := superDb.Close() diff --git a/test-integration.sh b/test-integration.sh deleted file mode 100755 index 9aad0fa..0000000 --- a/test-integration.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -export HOST=localhost -export EXIT_CODE=0 -export FEEDS=http://localhost:8123 -export DATABASE_URL="postgres://starter:starter@localhost:5432/starter_integration?sslmode=disable" - -echo "PROGRESS - creating databases" -psql postgres < ./databases/create_databases.sql - -echo "PROGRESS - building" -rm -rf ./build -mkdir -p ./build -go build -o ./build/migrate ./cmd/migrate -go build -o ./build/cannedrss ./cmd/cannedrss -go build -o ./build/collector ./cmd/collector -go build -o ./build/analyzer ./cmd/analyzer -go build -o ./build/app ./cmd/app - -echo "PROGRESS - migrating" -./build/migrate - -echo "PROGRESS - starting rss" -./build/cannedrss & -RSS_PID=$! - -echo "PROGRESS - collecting" -./build/collector -echo "PROGRESS - analyzing" -./build/analyzer -echo "PROGRESS - starting app" -PORT=8234 ./build/app & -APP_PID=$! - -sleep 1 - -echo "PROGRESS - GET /" -curl --fail-with-body http://localhost:8234 -EXIT_CODE=$((EXIT_CODE + $?)) - -echo "PROGRESS - POST /" -curl -XPOST -N --fail-with-body http://localhost:8234 -d"query=tell%20me%20about%20pickles" -EXIT_CODE=$((EXIT_CODE + $?)) - -echo "PROGRESS - killing app" -kill $APP_PID -echo "PROGRESS - killing rss" -kill $RSS_PID - -echo "" -if [ $EXIT_CODE -eq 0 ]; then echo "FINISHED - SUCCESS"; else echo "FINISHED - FAILURE"; fi -exit $EXIT_CODE