HH-448: enforce PostgreSQL E2E gate (#87)
* HH-448 enforce PostgreSQL E2E gate * HH-448 fail CI when concurrency test is missing --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
@@ -49,11 +49,11 @@ jobs:
|
||||
run: go vet ./...
|
||||
|
||||
backend-postgres:
|
||||
name: Backend (PostgreSQL concurrency)
|
||||
name: Backend (PostgreSQL)
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_DB: gochat_test
|
||||
POSTGRES_USER: postgres
|
||||
@@ -76,7 +76,67 @@ jobs:
|
||||
env:
|
||||
GOCHAT_TEST_DB: postgres
|
||||
GOCHAT_TEST_DB_URL: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable
|
||||
run: go test ./internal/autoassignment -run '^TestAssignmentServiceOnlyOneConcurrentWorkerWinsPostgres$' -count=3
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output="$RUNNER_TEMP/autoassignment-concurrency.json"
|
||||
go test -json ./internal/autoassignment -run '^TestAssignmentServiceOnlyOneConcurrentWorkerWinsPostgres$' -count=3 | tee "$output"
|
||||
grep -Eq '"Action":"pass".*"Test":"TestAssignmentServiceOnlyOneConcurrentWorkerWinsPostgres"' "$output" || {
|
||||
echo "PostgreSQL auto-assignment concurrency test did not run" >&2
|
||||
exit 1
|
||||
}
|
||||
- name: Test PostgreSQL E2E
|
||||
working-directory: backend
|
||||
env:
|
||||
GOCHAT_TEST_DB: postgres
|
||||
GOCHAT_TEST_DB_URL: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output="$RUNNER_TEMP/postgres-e2e.json"
|
||||
go test -json -count=1 -timeout 10m ./tests/e2e/... | tee "$output"
|
||||
|
||||
python3 - "$output" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
required = {
|
||||
"TestAuthE2ESuite",
|
||||
"TestAccountCRUDE2ESuite",
|
||||
"TestConversationE2ESuite",
|
||||
"TestCRME2ESuite",
|
||||
"TestCSRFE2ETestSuite",
|
||||
"TestRBACE2ESuite",
|
||||
}
|
||||
results = {}
|
||||
with open(sys.argv[1], encoding="utf-8") as events:
|
||||
for line in events:
|
||||
event = json.loads(line)
|
||||
if event.get("Test") and event.get("Action") in {"pass", "skip", "fail"}:
|
||||
results[event["Test"]] = event["Action"]
|
||||
|
||||
missing = sorted(suite for suite in required if results.get(suite) != "pass")
|
||||
passed_subtests = {suite: 0 for suite in required}
|
||||
skipped_subtests = {suite: 0 for suite in required}
|
||||
for test, action in results.items():
|
||||
suite, separator, _ = test.partition("/")
|
||||
if separator and suite in required:
|
||||
if action == "pass":
|
||||
passed_subtests[suite] += 1
|
||||
elif action == "skip":
|
||||
skipped_subtests[suite] += 1
|
||||
|
||||
empty = sorted(suite for suite, count in passed_subtests.items() if count == 0)
|
||||
for suite in sorted(required):
|
||||
print(f"{suite}: {passed_subtests[suite]} passed, {skipped_subtests[suite]} skipped")
|
||||
print(
|
||||
"E2E totals: "
|
||||
f"{sum(action == 'pass' for action in results.values())} passed, "
|
||||
f"{sum(action == 'skip' for action in results.values())} skipped"
|
||||
)
|
||||
if missing:
|
||||
raise SystemExit(f"required E2E suites did not pass: {', '.join(missing)}")
|
||||
if empty:
|
||||
raise SystemExit(f"required E2E suites ran no passing subtests: {', '.join(empty)}")
|
||||
PY
|
||||
|
||||
frontend:
|
||||
name: Frontend Build
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
@@ -38,12 +39,9 @@ func RunMigrations(dbURL string, migrationsPath string) error {
|
||||
}
|
||||
|
||||
func runMigrations(dbURL string, migrationsPath string) error {
|
||||
m, err := migrate.New(
|
||||
fmt.Sprintf("file://%s", migrationsPath),
|
||||
dbURL,
|
||||
)
|
||||
m, err := newMigrate(dbURL, migrationsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
@@ -95,12 +93,9 @@ func withSanitizedPQEnvironment(fn func() error) error {
|
||||
|
||||
// MigrateSteps applies N migration steps (positive = up, negative = down).
|
||||
func MigrateSteps(dbURL string, migrationsPath string, steps int) error {
|
||||
m, err := migrate.New(
|
||||
fmt.Sprintf("file://%s", migrationsPath),
|
||||
dbURL,
|
||||
)
|
||||
m, err := newMigrate(dbURL, migrationsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
@@ -116,16 +111,9 @@ func MigrateSteps(dbURL string, migrationsPath string, steps int) error {
|
||||
|
||||
// RollbackMigrations rolls back all migrations (drops all tables).
|
||||
func RollbackMigrations(dbURL string, migrationsPath string) error {
|
||||
if migrationsPath == "" {
|
||||
return errors.New("migrations path is required")
|
||||
}
|
||||
|
||||
m, err := migrate.New(
|
||||
fmt.Sprintf("file://%s", migrationsPath),
|
||||
dbURL,
|
||||
)
|
||||
m, err := newMigrate(dbURL, migrationsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
@@ -142,12 +130,9 @@ func RollbackMigrations(dbURL string, migrationsPath string) error {
|
||||
// ForceVersion sets the migration version to a specific number, clearing dirty state.
|
||||
// Use this to recover from a partially-applied migration.
|
||||
func ForceVersion(dbURL string, migrationsPath string, version int) error {
|
||||
m, err := migrate.New(
|
||||
fmt.Sprintf("file://%s", migrationsPath),
|
||||
dbURL,
|
||||
)
|
||||
m, err := newMigrate(dbURL, migrationsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
@@ -161,12 +146,9 @@ func ForceVersion(dbURL string, migrationsPath string, version int) error {
|
||||
// CurrentVersion returns the current migration version and whether the DB is in a dirty state.
|
||||
// When no migrations have been applied (version 0), returns (0, false, nil) instead of an error.
|
||||
func CurrentVersion(dbURL string, migrationsPath string) (uint, bool, error) {
|
||||
m, err := migrate.New(
|
||||
fmt.Sprintf("file://%s", migrationsPath),
|
||||
dbURL,
|
||||
)
|
||||
m, err := newMigrate(dbURL, migrationsPath)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
return 0, false, err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
@@ -181,3 +163,15 @@ func CurrentVersion(dbURL string, migrationsPath string) (uint, bool, error) {
|
||||
|
||||
return version, dirty, nil
|
||||
}
|
||||
|
||||
func newMigrate(dbURL, migrationsPath string) (*migrate.Migrate, error) {
|
||||
if strings.TrimSpace(migrationsPath) == "" {
|
||||
return nil, errors.New("migration path must not be empty")
|
||||
}
|
||||
|
||||
m, err := migrate.New(fmt.Sprintf("file://%s", migrationsPath), dbURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,25 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrationCommandsRejectBlankPath(t *testing.T) {
|
||||
tests := map[string]func() error{
|
||||
"run": func() error { return RunMigrations("invalid-url", " \t") },
|
||||
"steps": func() error { return MigrateSteps("invalid-url", " \t", 1) },
|
||||
"rollback": func() error { return RollbackMigrations("invalid-url", " \t") },
|
||||
"force": func() error { return ForceVersion("invalid-url", " \t", 1) },
|
||||
"version": func() error {
|
||||
_, _, err := CurrentVersion("invalid-url", " \t")
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.EqualError(t, run(), "migration path must not be empty")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithSanitizedPQEnvironment(t *testing.T) {
|
||||
t.Setenv("PGSERVICE", "local-service")
|
||||
t.Setenv("PGSERVICEFILE", "/tmp/pg_service.conf")
|
||||
|
||||
Reference in New Issue
Block a user