Skip to content

Commit

Permalink
Feat/integration tests (#6)
Browse files Browse the repository at this point in the history
* 🐛 fix generated sources names

* ♻️ Get directive type definition from lib for more isolation

* 🏗️ Add api for integration tests

* 👷 run tests tasks

* ✅ add tests

* 🤡 Replace api with camouflage

* ✅ Add tests for noAuth and headers directives

* 👷 wait 30s for services to be ready before running tests

---------

Co-authored-by: Mbaye THIAM <[email protected]>
  • Loading branch information
mbthiam88 and Mbaye THIAM authored Apr 26, 2024
1 parent e4c61c6 commit 4248b43
Show file tree
Hide file tree
Showing 45 changed files with 2,676 additions and 73 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Run Integration Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
integration-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
services:
registry:
image: registry:2
ports:
- 5000:5000
steps:
- name: Check out the repo
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host

- name: Copy patches for Docker Buildx
run: cp -r patches/* packages/graphql-mesh/patches

- name: Build and push on local registry
id: docker_build
uses: docker/build-push-action@v5
with:
context: ./packages/graphql-mesh
push: true
tags: localhost:5000/test/graphql-mesh:latest
platforms: linux/amd64

- name: Setup services for testing purpose
run: export IMAGE_TAG=localhost:5000/test/graphql-mesh:latest && cd ./test/integration && docker compose up -d

- name: Wait for services to be ready
run: sleep 30

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: cd ./test/integration/tests && npm install

- name: Run tests
run: cd ./test/integration/tests && npm test
2 changes: 0 additions & 2 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
node-version: ${{ matrix.node-version }}

- name: Set up Node.js
uses: actions/setup-node@v3
Expand Down
12 changes: 12 additions & 0 deletions packages/directive-headers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,15 @@ export default class HeadersDirectiveTransform implements MeshTransform {
})
}
}

export const headersDirectiveTypeDef: string = /* GraphQL */ `
input Header {
key: String
value: String
}
"""
This directive is used to add headers to the request.
"""
directive @headers(input: [Header]) on FIELD
`
7 changes: 7 additions & 0 deletions packages/directive-no-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ export default class NoAuthDirectiveTransform implements MeshTransform {
})
}
}

export const noAuthDirectiveTypeDef: string = /* GraphQL */ `
"""
This directive is used to disable the authorization header for the request
"""
directive @noAuth on FIELD
`
1 change: 0 additions & 1 deletion packages/directive-spl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"build:cjs": "tsc --project tsconfig-cjs.json",
"build:esm": "tsc --project tsconfig-esm.json",
"build": "rm -rf _build && npm run build:esm && npm run build:cjs && node ./scripts/prepare-package-json",
"dev": "vite",
"pack": "npm run build && npm pack --pack-destination ../graphql-mesh/local-pkg",
"test": "vitest"
},
Expand Down
7 changes: 7 additions & 0 deletions packages/directive-spl/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@ export default class SplDirectiveTransform implements MeshTransform {
})
}
}

export const splDirectiveTypeDef: string = /* GraphQL */ `
"""
This is a very small, lightweight, straightforward and non-evaluated expression language to sort, filter and paginate arrays of maps.
"""
directive @SPL(query: String) on FIELD
`
14 changes: 1 addition & 13 deletions packages/directive-spl/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
/// <reference types="vitest" />
// Configure Vitest (https://vitest.dev/config/)
import { resolve } from 'path'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
// https://vitejs.dev/guide/build.html#library-mode
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'my-lib',
fileName: 'my-lib'
}
},
plugins: [dts()]
})
export default defineConfig({})
15 changes: 12 additions & 3 deletions packages/graphql-mesh/.meshrc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { YamlConfig } from '@graphql-mesh/types'
import ConfigFromSwaggers from './utils/ConfigFromSwaggers'
import { splDirectiveTypeDef } from 'directive-spl'
import { headersDirectiveTypeDef } from 'directive-headers'
import { noAuthDirectiveTypeDef } from 'directive-no-auth'

const configFromSwaggers = new ConfigFromSwaggers()
const { defaultConfig, additionalTypeDefs, sources } =
Expand All @@ -17,13 +20,19 @@ const config = <YamlConfig.Config>{
}
},
...(defaultConfig.transforms || [])
],
].filter(Boolean),
sources: [...sources],
additionalTypeDefs: [defaultConfig.additionalTypeDefs || '', ...additionalTypeDefs],
additionalTypeDefs: [
splDirectiveTypeDef,
headersDirectiveTypeDef,
noAuthDirectiveTypeDef,
additionalTypeDefs,
defaultConfig.additionalTypeDefs || ''
].filter(Boolean),
additionalResolvers: [
...(defaultConfig.additionalResolvers || []),
'./utils/additionalResolvers.ts'
]
].filter(Boolean)
}

export default config
1 change: 1 addition & 0 deletions packages/graphql-mesh/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ VOLUME /app/sources
VOLUME /app/config.yaml
VOLUME /app/transforms
VOLUME /app/plugins
VOLUME /app/resolvers

CMD [ "npm", "run", "serve" ]
Binary file modified packages/graphql-mesh/local-pkg/directive-headers-1.0.0.tgz
Binary file not shown.
Binary file modified packages/graphql-mesh/local-pkg/directive-no-auth-1.0.0.tgz
Binary file not shown.
Binary file modified packages/graphql-mesh/local-pkg/directive-spl-1.0.0.tgz
Binary file not shown.
Binary file not shown.
24 changes: 19 additions & 5 deletions packages/graphql-mesh/scripts/download-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,34 @@ let config = getConfig()
const sources = config?.sources?.filter((source) => source?.handler?.openapi) || []
const swaggers = sources.map((source) => source?.handler?.openapi?.source) || []

/**
* Get the name of generated from the source object
* @param {Record<string, unknown>} source
* @returns {string | undefined}
*/
const getFileName = (url: string): string | undefined => {
return sources.find((source) => source?.handler?.openapi?.source === url)?.name
}

/**
* Download the swagger from the given URL and save it to the sources folder
* @param {string} url
*/
const downSwaggerFromUrl = async (url: string): Promise<void> => {
const downSwaggerFromUrl = async (url: string | undefined, index: string): Promise<void> => {
if (!url) return Promise.resolve()
try {
const content: Record<string, unknown> = await readFileOrUrl(url, {
allowUnknownExtensions: true,
cwd: '.',
fetch: fetch,
importFn: null,
importFn: (mod) => import(mod),
logger: logger
})
const fileName = url.split('/').pop()
let fileName = getFileName(url) || `${index}-${url.split('/').pop()}`
if (!fileName.endsWith('.json')) {
fileName += '.json'
}

if (fileName) {
const filePath = `./sources/${fileName}`
writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf8')
Expand All @@ -37,7 +51,7 @@ const downSwaggerFromUrl = async (url: string): Promise<void> => {
* Download all the swaggers from the given URLs
* @param {string[]} swaggers
*/
const downloadSwaggers = (swaggers: string[]) => {
const downloadSwaggers = (swaggers: (string | undefined)[]) => {
logger.info(`Downloading ${swaggers.length} swaggers sources...`)

// Create the sources folder if it doesn't exist
Expand All @@ -46,7 +60,7 @@ const downloadSwaggers = (swaggers: string[]) => {
}

if (swaggers.length) {
swaggers.forEach(downSwaggerFromUrl)
swaggers.forEach((file, index) => downSwaggerFromUrl(file, index.toString()))
}
}

Expand Down
38 changes: 21 additions & 17 deletions packages/graphql-mesh/utils/ConfigFromSwaggers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { globSync } from 'glob'
import { readFileSync } from 'node:fs'
import { Catalog, Spec, SwaggerName, ConfigExtension } from '../types'
import { getConfig, getSourceOpenapiEnpoint } from './config'
import { getConfig, getSourceName, getSourceOpenapiEnpoint } from './config'
import { getAvailableTypes } from './swaggers'
import { mergeObjects } from './helpers'
import { generateTypeDefsAndResolversFromSwagger } from './swaggers'
import { directiveTypeDefs } from './directive-typedefs'

export default class ConfigFromSwaggers {
swaggers: SwaggerName[] = []
Expand All @@ -28,7 +27,7 @@ export default class ConfigFromSwaggers {
content?.['application/json']?.schema['$ref'] ?? content?.['*/*']?.schema['$ref']
const schema = ref?.replace('#/components/schemas/', '')
if (schema) {
acc[path] = [query?.operationId, schema, this.swaggers[i]]
acc[path] = [query?.operationId || '', schema, this.swaggers[i]]
}
})
return acc
Expand All @@ -37,18 +36,20 @@ export default class ConfigFromSwaggers {

getInterfacesWithChildren() {
this.specs.forEach((s) => {
const { schemas } = s.components
const entries = Object.entries(schemas).filter(([_, value]) =>
const { schemas } = s.components || {}
const entries = Object.entries(schemas || {}).filter(([_, value]) =>
Object.keys(value).includes('discriminator')
)
for (const [schemaKey, schemaValue] of entries) {
const mapping = schemaValue['discriminator']['mapping'] ?? {}
const mappingTypes = []
mappingTypes.push(
...Object.keys(mapping)
.filter((k) => k !== schemaKey)
.map((k) => mapping[k].replace('#/components/schemas/', ''))
)
const mapping: { [key: string]: string } = schemaValue['discriminator']['mapping'] ?? {}
const mappingTypes: string[] = []
if (Object.keys(mapping).length > 0) {
mappingTypes.push(
...Object.keys(mapping)
.filter((k) => k !== schemaKey)
.map((k) => mapping[k].replace('#/components/schemas/', ''))
)
}
if (this.interfacesWithChildren[schemaKey] === undefined) {
this.interfacesWithChildren[schemaKey] = mappingTypes
} else {
Expand All @@ -71,7 +72,8 @@ export default class ConfigFromSwaggers {
spec,
availableTypes,
this.getInterfacesWithChildren(),
this.catalog
this.catalog,
this.config
)
acc.typeDefs += typeDefs
acc.resolvers = mergeObjects(acc.resolvers, resolvers)
Expand All @@ -84,14 +86,16 @@ export default class ConfigFromSwaggers {
getOpenApiSources() {
return (
this.swaggers.map((source) => ({
name: source,
name: getSourceName(source, this.config),
handler: {
openapi: {
source,
endpoint: getSourceOpenapiEnpoint(source, this.config) || '{env.ENDPOINT}',
ignoreErrorResponses: true,
operationHeaders: {
Authorization: `{context.headers["authorization"]}`
Authorization: `{context.headers["authorization"]}`,
...(this.config.sources?.find((item) => source.includes(item.name))?.handler?.openapi
?.operationHeaders || {})
}
}
}
Expand All @@ -118,15 +122,15 @@ export default class ConfigFromSwaggers {

getMeshConfigFromSwaggers(): {
defaultConfig: any
additionalTypeDefs: string[]
additionalTypeDefs: string
additionalResolvers: any
sources: any[]
} {
const { typeDefs, resolvers } = this.createTypeDefsAndResolvers()

return {
defaultConfig: this.config,
additionalTypeDefs: [typeDefs, directiveTypeDefs],
additionalTypeDefs: typeDefs,
additionalResolvers: resolvers,
sources: [...this.getOpenApiSources(), ...this.getOtherSources()]
}
Expand Down
15 changes: 12 additions & 3 deletions packages/graphql-mesh/utils/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,17 @@ export const getSourceOpenapiEnpoint = (
source: string,
config: YamlConfig.Config
): string | undefined => {
const data = config.sources?.find((item) =>
item?.handler?.openapi?.source?.includes(source.split('/').pop())
)
const data = config.sources?.find((item) => source.includes(item.name))
return data?.handler.openapi?.endpoint
}

/** Get source name from config
* @param source {string} - source name
* @param config {YamlConfig.Config} - config object
* @returns {string} - source name
*
*/
export const getSourceName = (source: string, config: YamlConfig.Config): string => {
const data = config.sources?.find((item) => source.includes(item.name))
return data?.name || source
}
26 changes: 0 additions & 26 deletions packages/graphql-mesh/utils/directive-typedefs/index.ts

This file was deleted.

Loading

0 comments on commit 4248b43

Please sign in to comment.