diff --git a/.github/workflows/tests-integration.yaml b/.github/workflows/tests-integration.yaml index bf02ac8ea2..3a77d42865 100644 --- a/.github/workflows/tests-integration.yaml +++ b/.github/workflows/tests-integration.yaml @@ -25,7 +25,7 @@ jobs: strategy: matrix: # Divide integration tests into 3 shards, to run them in parallel. - shardIndex: [1, 2, 3] + shardIndex: [1, 2, 3, 'apollo-router'] env: DOCKER_REGISTRY: ${{ inputs.registry }}/${{ inputs.imageName }}/ @@ -68,10 +68,46 @@ jobs: run: | docker compose -f docker/docker-compose.community.yml -f ./integration-tests/docker-compose.integration.yaml --env-file ./integration-tests/.env ps - - name: run integration tests - timeout-minutes: 10 + ## ---- START ---- Apollo Router specific steps + + - if: matrix.shardIndex == 'apollo-router' + name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - if: matrix.shardIndex == 'apollo-router' + name: Install Rust + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1 + with: + toolchain: '1.91.1' + default: true + override: true + + - if: matrix.shardIndex == 'apollo-router' + name: Cache Rust + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 + + - if: matrix.shardIndex == 'apollo-router' + name: build apollo router + run: | + cargo build + + - if: matrix.shardIndex == 'apollo-router' + name: run apollo router integration tests + timeout-minutes: 30 + run: | + pnpm test:integration:apollo-router + + ## ---- END ---- Apollo Router specific steps + + - if: matrix.shardIndex != 'apollo-router' + name: run integration tests + timeout-minutes: 30 run: | - VITEST_MAX_THREADS=${{ steps.cpu-cores.outputs.count }} pnpm --filter integration-tests test:integration --shard=${{ matrix.shardIndex }}/3 + pnpm test:integration --shard=${{ matrix.shardIndex }}/3 + env: + VITEST_MAX_THREADS: ${{ steps.cpu-cores.outputs.count }} - name: log dump if: ${{ failure() }} diff --git a/integration-tests/package.json b/integration-tests/package.json index 0f201abdd1..8f5175a981 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -7,6 +7,7 @@ "prepare:env": "cd ../ && pnpm build:libraries && pnpm build:services", "start": "./local.sh", "test:integration": "vitest", + "test:integration:apollo-router": "TEST_APOLLO_ROUTER=1 vitest tests/apollo-router", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/integration-tests/tests/apollo-router/apollo-router.test.ts b/integration-tests/tests/apollo-router/apollo-router.test.ts new file mode 100644 index 0000000000..980d26a9c1 --- /dev/null +++ b/integration-tests/tests/apollo-router/apollo-router.test.ts @@ -0,0 +1,136 @@ +import { existsSync, rmSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { ProjectType } from 'testkit/gql/graphql'; +import { initSeed } from 'testkit/seed'; +import { getServiceHost } from 'testkit/utils'; +import { execa } from '@esm2cjs/execa'; + +describe('Apollo Router Integration', () => { + const getBaseEndpoint = () => + getServiceHost('server', 8082).then(v => `http://${v}/artifacts/v1/`); + const getAvailablePort = () => + new Promise(resolve => { + const server = createServer(); + server.listen(0, () => { + const address = server.address(); + if (address && typeof address === 'object') { + const port = address.port; + server.close(() => resolve(port)); + } + }); + }); + it('fetches the supergraph and sends usage reports', async () => { + const routerConfigPath = join(tmpdir(), `apollo-router-config-${Date.now()}.yaml`); + const endpointBaseUrl = await getBaseEndpoint(); + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createTargetAccessToken, createCdnAccess, target, waitForOperationsCollected } = + await createProject(ProjectType.Federation); + const writeToken = await createTargetAccessToken({}); + + // Publish Schema + const publishSchemaResult = await writeToken + .publishSchema({ + author: 'Arda', + commit: 'abc123', + sdl: /* GraphQL */ ` + type Query { + me: User + } + type User { + id: ID! + name: String! + } + `, + service: 'users', + url: 'https://federation-demo.theguild.workers.dev/users', + }) + .then(r => r.expectNoGraphQLErrors()); + + expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + const cdnAccessResult = await createCdnAccess(); + + const usageAddress = await getServiceHost('usage', 8081); + + const routerBinPath = join(__dirname, '../../../target/debug/router'); + if (!existsSync(routerBinPath)) { + throw new Error( + `Apollo Router binary not found at path: ${routerBinPath}, make sure to build it first with 'cargo build'`, + ); + } + const routerPort = await getAvailablePort(); + const routerConfigContent = ` +supergraph: + listen: 0.0.0.0:${routerPort} +plugins: + hive.usage: {} +`.trim(); + writeFileSync(routerConfigPath, routerConfigContent, 'utf-8'); + const routerProc = execa(routerBinPath, ['--dev', '--config', routerConfigPath], { + all: true, + env: { + HIVE_CDN_ENDPOINT: endpointBaseUrl + target.id, + HIVE_CDN_KEY: cdnAccessResult.secretAccessToken, + HIVE_ENDPOINT: `http://${usageAddress}`, + HIVE_TOKEN: writeToken.secret, + HIVE_TARGET_ID: target.id, + }, + }); + await new Promise((resolve, reject) => { + routerProc.catch(err => { + if (!err.isCanceled) { + reject(err); + } + }); + const routerProcOut = routerProc.all; + if (!routerProcOut) { + return reject(new Error('No stdout from Apollo Router process')); + } + let log = ''; + routerProcOut.on('data', data => { + log += data.toString(); + if (log.includes('GraphQL endpoint exposed at')) { + resolve(true); + } + }); + }); + + try { + const url = `http://localhost:${routerPort}/`; + const response = await fetch(url, { + method: 'POST', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + query: ` + query TestQuery { + me { + id + name + } + } + `, + }), + }); + + expect(response.status).toBe(200); + const result = await response.json(); + expect(result).toEqual({ + data: { + me: { + id: '1', + name: 'Ada Lovelace', + }, + }, + }); + await waitForOperationsCollected(1); + } finally { + routerProc.cancel(); + rmSync(routerConfigPath); + } + }); +}); diff --git a/integration-tests/vite.config.ts b/integration-tests/vite.config.ts index d14e9b16b7..857f741289 100644 --- a/integration-tests/vite.config.ts +++ b/integration-tests/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vitest/config'; +import { defaultExclude, defineConfig } from 'vitest/config'; const setupFiles = ['../scripts/serializer.ts', './expect.ts']; @@ -29,5 +29,8 @@ export default defineConfig({ }, setupFiles, testTimeout: 90_000, + exclude: process.env.TEST_APOLLO_ROUTER + ? defaultExclude + : [...defaultExclude, 'tests/apollo-router/**'], }, }); diff --git a/package.json b/package.json index 830e9de13c..7f10c66cf3 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "test:e2e:local": "CYPRESS_BASE_URL=http://localhost:3000 RUN_AGAINST_LOCAL_SERVICES=1 cypress open --browser chrome", "test:e2e:open": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress open", "test:integration": "cd integration-tests && pnpm test:integration", + "test:integration:apollo-router": "cd integration-tests && pnpm test:integration:apollo-router", "typecheck": "pnpm run -r --filter '!hive' typecheck", "upload-sourcemaps": "./scripts/upload-sourcemaps.sh", "workspace": "pnpm run --filter $1 $2"