diff --git a/Makefile b/Makefile index 569924bb..e6fd7e50 100644 --- a/Makefile +++ b/Makefile @@ -27,14 +27,37 @@ lint: unit_tests: go test -race $$(go list ./... | grep -v integration_tests) -integration_tests: build start_mongo +integration_tests: build start_postgres init_test_db go test -race -v ./integration_tests -start_mongo: - ./mongo.sh start +start_postgres: + @if [ -z $$(docker ps -aqf name=router-postgres-test-db) ]; then \ + docker run \ + --name router-postgres-test-db \ + -e POSTGRES_HOST_AUTH_METHOD=trust \ + -d \ + -p 5432:5432 \ + --user 'postgres' \ + --health-cmd 'pg_isready' \ + --health-start-period 5s \ + postgres:14; \ + echo Waiting for postgres to be up; \ + for _ in $$(seq 60); do \ + if [ "$$(docker inspect -f '{{.State.Health.Status}}' router-postgres-test-db)" = "healthy" ]; then \ + break; \ + fi; \ + echo '.\c'; \ + sleep 1; \ + done; \ + else \ + echo "PostgreSQL container 'router-postgres-test-db' already exists. Skipping creation."; \ + fi -stop_mongo: - ./mongo.sh stop +init_test_db: + docker exec -i router-postgres-test-db psql < localdb_init.sql + +cleanup_postgres: + @docker rm -f router-postgres-test-db || true update_deps: go get -t -u ./... && go mod tidy && go mod vendor diff --git a/integration_tests/route_helpers.go b/integration_tests/route_helpers.go index c195dc6f..452d7805 100644 --- a/integration_tests/route_helpers.go +++ b/integration_tests/route_helpers.go @@ -1,12 +1,12 @@ package integration import ( + "database/sql" "fmt" "os" "time" - "github.com/globalsign/mgo" - "github.com/globalsign/mgo/bson" + _ "github.com/lib/pq" // Without which we can't use PSQL calls // revive:disable:dot-imports . "github.com/onsi/ginkgo/v2" @@ -19,18 +19,18 @@ var _ = AfterEach(func() { }) var ( - routerDB *mgo.Database + routerDB *sql.DB ) type Route struct { - IncomingPath string `bson:"incoming_path"` - RouteType string `bson:"route_type"` - Handler string `bson:"handler"` - BackendID string `bson:"backend_id"` - RedirectTo string `bson:"redirect_to"` - RedirectType string `bson:"redirect_type"` - SegmentsMode string `bson:"segments_mode"` - Disabled bool `bson:"disabled"` + IncomingPath string + RouteType string + Handler string + BackendID string + RedirectTo string + RedirectType string + SegmentsMode string + Disabled bool } func NewBackendRoute(backendID string, extraParams ...string) Route { @@ -80,36 +80,61 @@ func NewGoneRoute(extraParams ...string) Route { } func initRouteHelper() error { - databaseURL := os.Getenv("ROUTER_MONGO_URL") + databaseURL := os.Getenv("DATABASE_URL") if databaseURL == "" { - databaseURL = "127.0.0.1" + databaseURL = "postgresql://postgres@127.0.0.1:5432/router_test?sslmode=disable" } - sess, err := mgo.Dial(databaseURL) + db, err := sql.Open("postgres", databaseURL) if err != nil { - return fmt.Errorf("failed to connect to mongo: %w", err) + return fmt.Errorf("Failed to connect to Postgres: " + err.Error()) } - sess.SetSyncTimeout(10 * time.Minute) - sess.SetSocketTimeout(10 * time.Minute) - routerDB = sess.DB("router_test") + db.SetConnMaxLifetime(10 * time.Minute) + db.SetMaxIdleConns(0) + db.SetMaxOpenConns(10) + + routerDB = db return nil } func addBackend(id, url string) { - err := routerDB.C("backends").Insert(bson.M{"backend_id": id, "backend_url": url}) - Expect(err).NotTo(HaveOccurred()) + query := ` + INSERT INTO backends (backend_id, backend_url, created_at, updated_at) + VALUES ($1, $2, $3, $4) + ` + + _, err := routerDB.Exec(query, id, url, time.Now(), time.Now()) + Expect(err).ToNot(HaveOccurred()) } func addRoute(path string, route Route) { route.IncomingPath = path - err := routerDB.C("routes").Insert(route) - Expect(err).NotTo(HaveOccurred()) + query := ` + INSERT INTO routes (incoming_path, route_type, handler, backend_id, redirect_to, redirect_type, segments_mode, disabled, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ` + + _, err := routerDB.Exec( + query, + route.IncomingPath, + route.RouteType, + route.Handler, + route.BackendID, + route.RedirectTo, + route.RedirectType, + route.SegmentsMode, + route.Disabled, + time.Now(), + time.Now(), + ) + + Expect(err).ToNot(HaveOccurred()) } func clearRoutes() { - _ = routerDB.C("routes").DropCollection() - _ = routerDB.C("backends").DropCollection() + _, err := routerDB.Exec("DELETE FROM routes; DELETE FROM backends") + Expect(err).ToNot(HaveOccurred()) } diff --git a/integration_tests/router_support.go b/integration_tests/router_support.go index d03d526f..e5d22e70 100644 --- a/integration_tests/router_support.go +++ b/integration_tests/router_support.go @@ -17,8 +17,8 @@ import ( ) const ( - routerPort = 3169 - apiPort = 3168 + routerPort = 5434 + apiPort = 5433 ) func routerURL(port int, path string) string { @@ -56,7 +56,7 @@ func startRouter(port, apiPort int, extraEnv []string) error { } cmd := exec.Command(bin) - cmd.Env = append(cmd.Environ(), "ROUTER_MONGO_DB=router_test") + cmd.Env = append(cmd.Environ(), "DATABASE_NAME=router_test") cmd.Env = append(cmd.Env, fmt.Sprintf("ROUTER_PUBADDR=%s", pubAddr)) cmd.Env = append(cmd.Env, fmt.Sprintf("ROUTER_APIADDR=%s", apiAddr)) cmd.Env = append(cmd.Env, fmt.Sprintf("ROUTER_ERROR_LOG=%s", tempLogfile.Name())) diff --git a/lib/router_test.go b/lib/router_test.go index 5a11ce88..d31c8528 100644 --- a/lib/router_test.go +++ b/lib/router_test.go @@ -1,171 +1,13 @@ package router import ( - "errors" "testing" - "time" - - "github.com/globalsign/mgo/bson" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -type mockMongoDB struct { - result bson.M - err error -} - -func (m *mockMongoDB) Run(_ interface{}, res interface{}) error { - if m.err != nil { - return m.err - } - - bytes, err := bson.Marshal(m.result) - if err != nil { - return err - } - - err = bson.Unmarshal(bytes, res) - if err != nil { - return err - } - - return nil -} - func TestRouter(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Router Suite") } - -var _ = Describe("Router", func() { - Context("When calling shouldReload", func() { - Context("with an up-to-date mongo instance", func() { - It("should return false", func() { - rt := Router{} - initialOptime, _ := bson.NewMongoTimestamp(time.Date(2021, time.March, 12, 8, 0, 0, 0, time.UTC), 1) - rt.mongoReadToOptime = initialOptime - - currentOptime, _ := bson.NewMongoTimestamp(time.Date(2021, time.March, 12, 8, 0, 0, 0, time.UTC), 1) - mongoInstance := MongoReplicaSetMember{} - mongoInstance.Optime = currentOptime - - Expect(rt.shouldReload(mongoInstance)).To( - BeFalse(), - "Router should determine no reload is necessary when Mongo optime hasn't changed", - ) - }) - }) - - Context("with a stale mongo instance", func() { - It("should return false when timestamp differs", func() { - rt := Router{} - initialOptime, _ := bson.NewMongoTimestamp(time.Date(2021, time.March, 12, 8, 0, 0, 0, time.UTC), 1) - rt.mongoReadToOptime = initialOptime - - currentOptime, _ := bson.NewMongoTimestamp(time.Date(2021, time.March, 12, 8, 2, 30, 0, time.UTC), 1) - mongoInstance := MongoReplicaSetMember{} - mongoInstance.Optime = currentOptime - - Expect(rt.shouldReload(mongoInstance)).To( - BeTrue(), - "Router should determine reload is necessary when Mongo optime has changed by timestamp", - ) - }) - - It("should return false when operand differs", func() { - rt := Router{} - initialOptime, _ := bson.NewMongoTimestamp(time.Date(2021, time.March, 12, 8, 0, 0, 0, time.UTC), 1) - rt.mongoReadToOptime = initialOptime - - currentOptime, _ := bson.NewMongoTimestamp(time.Date(2021, time.March, 12, 8, 0, 0, 0, time.UTC), 2) - mongoInstance := MongoReplicaSetMember{} - mongoInstance.Optime = currentOptime - - Expect(rt.shouldReload(mongoInstance)).To( - BeTrue(), - "Router should determine reload is necessary when Mongo optime has changed by operand", - ) - }) - }) - }) - - Context("When calling getCurrentMongoInstance", func() { - It("should return error when unable to get the replica set", func() { - mockMongoObj := &mockMongoDB{ - err: errors.New("Error connecting to replica set"), - } - - rt := Router{} - _, err := rt.getCurrentMongoInstance(mockMongoObj) - - Expect(err).To( - HaveOccurred(), - "Router should raise an error when it can't get replica set status from Mongo") - }) - - It("should return fail to find an instance when the replica set status schema doesn't match the expected schema", func() { - replicaSetStatusBson := bson.M{"members": []bson.M{{"unknownProperty": "unknown"}}} - mockMongoObj := &mockMongoDB{ - result: replicaSetStatusBson, - } - - rt := Router{} - _, err := rt.getCurrentMongoInstance(mockMongoObj) - - Expect(err).To( - HaveOccurred(), - "Router should raise an error when the current Mongo instance can't be found in the replica set status response") - }) - - It("should return fail to find an instance when the replica set status contains no instances marked with self:true", func() { - replicaSetStatusBson := bson.M{"members": []bson.M{{"name": "mongo1", "self": false}}} - mockMongoObj := &mockMongoDB{ - result: replicaSetStatusBson, - } - - rt := Router{} - _, err := rt.getCurrentMongoInstance(mockMongoObj) - - Expect(err).To( - HaveOccurred(), - "Router should raise an error when the current Mongo instance can't be found in the replica set status response") - }) - - It("should return fail to find an instance when the replica set status contains multiple instances marked with self:true", func() { - replicaSetStatusBson := bson.M{"members": []bson.M{{"name": "mongo1", "self": true}, {"name": "mongo2", "self": true}}} - mockMongoObj := &mockMongoDB{ - result: replicaSetStatusBson, - } - - rt := Router{} - _, err := rt.getCurrentMongoInstance(mockMongoObj) - - Expect(err).To( - HaveOccurred(), - "Router should raise an error when the replica set status response contains multiple current Mongo instances") - }) - - It("should successfully return the current Mongo instance from the replica set", func() { - replicaSetStatusBson := bson.M{"members": []bson.M{{"name": "mongo1", "self": false}, {"name": "mongo2", "optime": 6945383634312364034, "self": true}}} - mockMongoObj := &mockMongoDB{ - result: replicaSetStatusBson, - } - - expectedMongoInstance := MongoReplicaSetMember{ - Name: "mongo2", - Optime: 6945383634312364034, - Current: true, - } - - rt := Router{} - currentMongoInstance, _ := rt.getCurrentMongoInstance(mockMongoObj) - - Expect(currentMongoInstance).To( - Equal(expectedMongoInstance), - "Router should get the current Mongo instance from the replica set status response", - ) - }) - }) -}) diff --git a/localdb_init.sql b/localdb_init.sql new file mode 100644 index 00000000..f9d844e5 --- /dev/null +++ b/localdb_init.sql @@ -0,0 +1,67 @@ +DROP DATABASE IF EXISTS router_test; +CREATE DATABASE router_test; +\connect router_test; + +CREATE TABLE backends ( + id SERIAL PRIMARY KEY, + backend_id VARCHAR, + backend_url VARCHAR, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); +CREATE UNIQUE INDEX index_backends_on_backend_id ON backends (backend_id); + +CREATE TABLE routes ( + id SERIAL PRIMARY KEY, + incoming_path VARCHAR, + route_type VARCHAR, + handler VARCHAR, + disabled BOOLEAN DEFAULT false, + backend_id VARCHAR, + redirect_to VARCHAR, + redirect_type VARCHAR, + segments_mode VARCHAR, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); +CREATE UNIQUE INDEX index_unique_routes ON routes (incoming_path, route_type); + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR, + email VARCHAR, + uid VARCHAR, + organisation_slug VARCHAR, + organisation_content_id VARCHAR, + app_name VARCHAR, + permissions TEXT, + remotely_signed_out BOOLEAN DEFAULT false, + disabled BOOLEAN DEFAULT false, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); + +CREATE OR REPLACE FUNCTION notify_listeners() RETURNS TRIGGER AS $$ + BEGIN + PERFORM pg_notify('notify', 'notification from test database'); + RETURN NULL; + END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER backends_notify_trigger +AFTER INSERT OR UPDATE OR DELETE +ON routes +FOR EACH ROW +EXECUTE PROCEDURE notify_listeners(); + +CREATE TRIGGER routes_notify_trigger +AFTER INSERT OR UPDATE OR DELETE +ON backends +FOR EACH ROW +EXECUTE PROCEDURE notify_listeners(); + +CREATE TRIGGER users_notify_trigger +AFTER INSERT OR UPDATE OR DELETE +ON users +FOR EACH ROW +EXECUTE PROCEDURE notify_listeners(); \ No newline at end of file