diff --git a/.github/workflows/dsg-e2e-tests.yml b/.github/workflows/dsg-e2e-tests.yml index eba88f9..9602698 100644 --- a/.github/workflows/dsg-e2e-tests.yml +++ b/.github/workflows/dsg-e2e-tests.yml @@ -76,11 +76,8 @@ jobs: exit 1 fi - - name: Run generated code - run: | - cd test-cases/${{ matrix.test-cases }}/generated/server - docker-compose up -d - - name: Run e2e tests run: npm run e2e:test + env: + TEST_CASE: ${{ matrix.test-cases }} diff --git a/jest.config.ts b/jest.config.ts index c5e46bb..6423cca 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,6 +3,7 @@ import type { Config } from "jest"; const config: Config = { preset: "ts-jest", testEnvironment: "node", + testTimeout: 10000000, }; export default config; diff --git a/package-lock.json b/package-lock.json index 69fc114..341efb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", "@types/uuid": "^9.0.8", + "docker-compose": "^0.24.7", + "get-port-please": "^3.1.2", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", @@ -1826,6 +1828,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/docker-compose": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.7.tgz", + "integrity": "sha512-CdHl9n0S4+bl4i6MaxDQHNjqB1FdvuDirrDTzPKmdiMpheQqCjgsny0GZ2VhvN7qHTY0833lRlKWZgrkn1i6cg==", + "dev": true, + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.721", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.721.tgz", @@ -2034,6 +2048,12 @@ "node": ">=8.0.0" } }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "dev": true + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4177,6 +4197,18 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index d4ca2f2..51b6645 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", "@types/uuid": "^9.0.8", + "docker-compose": "^0.24.7", + "get-port-please": "^3.1.2", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", diff --git a/tests/data-service-generator.e2e.spec.ts b/tests/data-service-generator.e2e.spec.ts index ce348dc..272fb8e 100644 --- a/tests/data-service-generator.e2e.spec.ts +++ b/tests/data-service-generator.e2e.spec.ts @@ -1,5 +1,3 @@ -import * as os from "os"; -import * as fs from "fs"; import * as path from "path"; import { ApolloClient, @@ -12,19 +10,15 @@ import { onError } from "@apollo/client/link/error"; import fetch from "cross-fetch"; import { omit } from "lodash"; import env from "../test-data/env"; -import { v4 } from "uuid"; - -const SERVER_START_TIMEOUT = 30000; +import { v2 as compose } from "docker-compose"; +import { getRandomPort } from "get-port-please"; const JSON_MIME = "application/json"; const STATUS_OK = 200; -const STATUS_NO_CONTENT = 204; const STATUS_CREATED = 201; const NOT_FOUND = 404; const { - APP_HOST, - APP_PORT, APP_USERNAME, APP_PASSWORD, APP_DEFAULT_USER_ROLES, @@ -42,23 +36,82 @@ const EXAMPLE_ORGANIZATION = { name: "Amplication", }; +const testCaseName = process.env.TEST_CASE || "postgres-basic"; +const verbose = process.env.VERBOSE ? true : false; +const SERVER_START_TIMEOUT = 30000; + +function sleep(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)); +} + describe("Data Service Generator", () => { - const host = `${APP_HOST}:${APP_PORT}`; + let host: string; let customer: { id: number }; let apolloClient: ApolloClient; describe("DSG E2E tests", () => { - const testId = v4(); - beforeAll(async () => { - const directory = path.join(os.tmpdir(), "test-data-service", testId); - // Clean the temporary directory - try { - await fs.promises.rm(directory, { recursive: true }); - } catch { - /* empty */ - } - await fs.promises.mkdir(directory, { recursive: true }); + console.info("Setting up test environment..."); + const dockerComposeDir = path.resolve( + __dirname, + `../test-cases/${testCaseName}/generated/server` + ); + const dotEnvPath = path.join(dockerComposeDir, ".env"); + const port = await getRandomPort(); + const dbPort = await getRandomPort(); + host = `localhost:${port}`; + + const dockerComposeOptions: compose.IDockerComposeOptions = { + cwd: dockerComposeDir, + log: verbose, + composeOptions: [ + `--project-name=${testCaseName}`, + `--env-file=${dotEnvPath}`, + ], + env: { + ...process.env, + PORT: String(port), + DB_PORT: String(dbPort), + }, + }; + + console.info("Running docker-compose up"); + await compose.downAll(dockerComposeOptions); + await compose.upAll({ + ...dockerComposeOptions, + commandOptions: ["--build", "--force-recreate"], + }); + + compose + .logs([], { + ...dockerComposeOptions, + follow: true, + }) + .catch(console.error); + + console.info("Waiting for db migration to be completed..."); + let migrationCompleted = false; + let startTime = Date.now(); + + do { + console.info("..."); + const containers = await compose.ps({ + ...dockerComposeOptions, + commandOptions: ["--all"], + }); + const migrateContainer = containers.data.services.find((s) => + s.name.endsWith("migrate-1") + ); + if (migrateContainer?.state.indexOf("Exited (0)") !== -1) { + migrationCompleted = true; + console.info("migration completed!"); + break; + } + await sleep(2000); + } while ( + !migrationCompleted || + startTime + SERVER_START_TIMEOUT < Date.now() + ); const authLink = setContext((_, { headers }) => ({ headers: { @@ -90,28 +143,18 @@ describe("Data Service Generator", () => { link: authLink.concat(errorLink).concat(httpLink), cache: new InMemoryCache(), }); + }); - console.log("Waiting for server to be ready..."); - let servicesNotReady = true; - let startTime = Date.now(); - do { - console.log("..."); - try { - const res = await fetch(`http://${host}/api/_health/live`, { - method: "GET", - }); - if (res.status === STATUS_NO_CONTENT) { - servicesNotReady = false; - console.log("server ready!"); - break; - } - } catch (error) { - /**/ - } - } while ( - servicesNotReady || - startTime + SERVER_START_TIMEOUT < Date.now() - ); + afterAll(async () => { + console.info("Tearing down test environment..."); + const dockerComposeDir = path.resolve(__dirname, "../.."); + const dockerComposeOptions: compose.IDockerComposeOptions = { + cwd: dockerComposeDir, + composeOptions: [`--project-name=${testCaseName}`], + commandOptions: ["-v"], + }; + + await compose.downAll(dockerComposeOptions); }); it("check /api/health/live endpoint", async () => { @@ -272,7 +315,10 @@ describe("Data Service Generator", () => { "Content-Type": JSON_MIME, Authorization: APP_BASIC_AUTHORIZATION, }, - body: JSON.stringify(EXAMPLE_CUSTOMER), + body: JSON.stringify({ + ...EXAMPLE_CUSTOMER, + email: "test-rest@test.com", + }), }); const newCustomer = await newCustomerRes.json(); @@ -291,6 +337,7 @@ describe("Data Service Generator", () => { expect.objectContaining({ ...EXAMPLE_CUSTOMER, id: expect.any(String), + email: "test-rest@test.com", createdAt: expect.any(String), updatedAt: expect.any(String), }) @@ -343,7 +390,10 @@ describe("Data Service Generator", () => { } `, variables: { - data: EXAMPLE_CUSTOMER, + data: { + ...EXAMPLE_CUSTOMER, + email: `test-gql@example.com`, + }, }, }); expect(resp).toEqual( @@ -351,7 +401,7 @@ describe("Data Service Generator", () => { data: { createCustomer: expect.objectContaining({ id: expect.any(String), - email: EXAMPLE_CUSTOMER.email, + email: "test-gql@example.com", }), }, }) @@ -373,7 +423,10 @@ describe("Data Service Generator", () => { "Content-Type": JSON_MIME, Authorization: APP_BASIC_AUTHORIZATION, }, - body: JSON.stringify(EXAMPLE_CUSTOMER), + body: JSON.stringify({ + ...EXAMPLE_CUSTOMER, + email: "test-org@post.com", + }), }) ).json(); @@ -416,7 +469,10 @@ describe("Data Service Generator", () => { "Content-Type": JSON_MIME, Authorization: APP_BASIC_AUTHORIZATION, }, - body: JSON.stringify(EXAMPLE_CUSTOMER), + body: JSON.stringify({ + ...EXAMPLE_CUSTOMER, + email: "test-org@delete.com", + }), }) ).json(); const organization = await ( @@ -474,7 +530,10 @@ describe("Data Service Generator", () => { "Content-Type": JSON_MIME, Authorization: APP_BASIC_AUTHORIZATION, }, - body: JSON.stringify(EXAMPLE_CUSTOMER), + body: JSON.stringify({ + ...EXAMPLE_CUSTOMER, + email: "test-org@get.com", + }), }) ).json(); const organization = await ( @@ -521,6 +580,7 @@ describe("Data Service Generator", () => { expect.objectContaining({ ...EXAMPLE_CUSTOMER, id: customer.id, + email: "test-org@get.com", createdAt: expect.any(String), updatedAt: expect.any(String), organization: {