From 67360f5ad5091f5e115a9c353d0104173c7301a2 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Fri, 11 Mar 2022 12:23:38 -0800 Subject: [PATCH] refactor rest code (#25879) --- lib/rest/index.js | 124 ++++--- .../[versionId]/rest/reference/[category].tsx | 60 +--- tests/rendering/rest.js | 52 +-- tests/unit/openapi-schema.js | 310 +++++++++++------- 4 files changed, 275 insertions(+), 271 deletions(-) diff --git a/lib/rest/index.js b/lib/rest/index.js index 9228284cd7f1..93c614f91c2b 100644 --- a/lib/rest/index.js +++ b/lib/rest/index.js @@ -1,16 +1,15 @@ import { fileURLToPath } from 'url' import path from 'path' -import fs from 'fs' import { readCompressedJsonFileFallback } from '../read-json-file.js' import renderContent from '../render-content/index.js' import getMiniTocItems from '../get-mini-toc-items.js' import { allVersions } from '../all-versions.js' -import { get } from 'lodash-es' +import languages from '../languages.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const schemasPath = path.join(__dirname, 'static/decorated') const ENABLED_APPS_FILENAME = path.join(__dirname, 'static/apps/enabled-for-apps.json') -/* +/* Loads the schemas from the static/decorated folder into a single object organized by version. @@ -23,23 +22,33 @@ const ENABLED_APPS_FILENAME = path.join(__dirname, 'static/apps/enabled-for-apps } } */ -export default async function getRest() { - const operations = {} - fs.readdirSync(schemasPath).forEach((filename) => { - // In staging deploys, the `.json` files might have been converted to - // to `.json.br`. In that case, we need to also remove the `.json` - // extension from the key - const key = path.parse(filename).name.replace('.json', '') +const restOperationData = new Map() +Object.keys(languages).forEach((language) => { + restOperationData.set(language, new Map()) + Object.keys(allVersions).forEach((version) => { + // setting to undefined will allow us to perform checks + // more easily later on + restOperationData.get(language).set(version, new Map()) + }) +}) + +const restOperations = new Map() +export default async function getRest(version, category) { + const openApiVersion = getOpenApiVersion(version) + if (!restOperations.has(openApiVersion)) { + const filename = `${openApiVersion}.json` + // The `readCompressedJsonFileFallback()` function // will check for both a .br and .json extension. - const value = readCompressedJsonFileFallback(path.join(schemasPath, filename)) - const docsVersions = getDocsVersions(key) - docsVersions.forEach((docsVersion) => { - operations[docsVersion] = value - }) - }) + restOperations.set( + openApiVersion, + readCompressedJsonFileFallback(path.join(schemasPath, filename)) + ) + } - return operations + return category + ? restOperations.get(openApiVersion)[category] + : restOperations.get(openApiVersion) } // Right now there is not a 1:1 mapping of openapi to docs versions, @@ -52,48 +61,57 @@ function getDocsVersions(openApiVersion) { return versions } -// Used for units tests -export function getFlatListOfOperations(operations) { - const flatList = [] - Object.keys(operations).forEach((version) => { - Object.keys(operations[version]).forEach((category) => { - Object.keys(operations[version][category]).forEach((subcategory) => { - flatList.push(...operations[version][category][subcategory]) - }) - }) - }) - return flatList +function getOpenApiVersion(version) { + if (!(version in allVersions)) { + throw new Error(`Unrecognized version '${version}'. Not found in ${Object.keys(allVersions)}`) + } + return allVersions[version].openApiVersionName } // Generates the rendered Markdown from the data files in the // data/reusables/rest-reference directory for a given category // and generates the miniToc for a rest reference page. -export async function getRestOperationData(category, categoryOperations, context) { - const descriptions = {} - let toc = '' - const reusablePath = context.site.data.reusables - const subcategories = Object.keys(categoryOperations) - let introContent = null - for (const subcategory of subcategories) { - const markdown = get(reusablePath['rest-reference'][category], subcategory) - const renderedMarkdown = await renderContent(markdown, context) - descriptions[subcategory] = renderedMarkdown - // only a string with the raw HTML of each heading level 2 and 3 - // is needed to generate the toc - const titles = categoryOperations[subcategory] - .map((operation) => `### ${operation.summary}\n`) - .join('') - toc += renderedMarkdown + (await renderContent(titles, context)) - } - // This is Markdown content at the path - // data/reusables/rest-reference// - // that doesn't map directory to a group of operations. - if (get(reusablePath['rest-reference'][category], category) && !categoryOperations[category]) { - const markdown = get(reusablePath['rest-reference'][category], category) - introContent = await renderContent(markdown, context) +export async function getRestOperationData(category, language, version, context) { + if (!restOperationData.get(language).get(version).has(category)) { + const languageTree = restOperationData.get(language) + const descriptions = {} + let toc = '' + const reusablePath = context.site.data.reusables + const categoryOperations = await getRest(version, category) + const subcategories = Object.keys(categoryOperations) + let introContent = null + for (const subcategory of subcategories) { + const markdown = reusablePath['rest-reference']?.[category]?.[subcategory] + const renderedMarkdown = await renderContent(markdown, context) + descriptions[subcategory] = renderedMarkdown + // only a string with the raw HTML of each heading level 2 and 3 + // is needed to generate the toc + const titles = categoryOperations[subcategory] + .map((operation) => `### ${operation.summary}\n`) + .join('') + toc += renderedMarkdown + (await renderContent(titles, context)) + } + // Usually a Markdown file in + // data/resuables/rest-reference// + // will always map to a set of operations. But sometimes we have + // introductory text that doesn't map to any operations. + // When that is the case, the category and subcategory are the same. + // Example data/resuables/rest-reference/actions/actions + // The content in this file is called introContent and is displayed + // at the top of the rest reference page. + if (reusablePath['rest-reference']?.[category]?.[category] && !categoryOperations[category]) { + const markdown = reusablePath['rest-reference']?.[category]?.[category] + introContent = await renderContent(markdown, context) + } + const miniTocItems = getMiniTocItems(toc, 3) + languageTree.get(version).set(category, { + descriptions, + miniTocItems, + introContent, + }) + restOperationData.set(restOperationData, languageTree) } - const miniTocItems = getMiniTocItems(toc, 3) - return { descriptions, miniTocItems, introContent } + return restOperationData.get(language).get(version).get(category) } export async function getEnabledForApps() { diff --git a/pages/[versionId]/rest/reference/[category].tsx b/pages/[versionId]/rest/reference/[category].tsx index 87dd2ba0e1b6..369e705c9bca 100644 --- a/pages/[versionId]/rest/reference/[category].tsx +++ b/pages/[versionId]/rest/reference/[category].tsx @@ -5,12 +5,6 @@ import { RestCategoryOperationsT } from 'components/rest/types' import { MiniTocItem } from 'components/context/ArticleContext' import { RestReferencePage } from 'components/rest/RestReferencePage' -type RestOperationsT = { - [version: string]: { - [category: string]: RestCategoryOperationsT - } -} - type CategoryDataT = { descriptions: { [subcategory: string]: string @@ -19,14 +13,6 @@ type CategoryDataT = { introContent: string } -type RestDataT = { - [language: string]: { - [version: string]: { - [category: string]: CategoryDataT - } - } -} - type Props = { mainContext: MainContextT restOperations: RestCategoryOperationsT @@ -35,9 +21,6 @@ type Props = { introContent: string } -let rest: RestOperationsT | null = null -let restOperationData: RestDataT | null = null - export default function Category({ mainContext, restOperations, @@ -65,48 +48,25 @@ export const getServerSideProps: GetServerSideProps = async (context) => const currentVersion = context.params!.versionId as string const currentLanguage = req.context.currentLanguage as string - // Use a local cache to store all of the REST operations, so - // we only read the directory of static/decorated files once - if (!rest) { - rest = (await getRest()) as RestOperationsT - } - - /* This sets up a skeleton object in the format: - { - 'en': { free-pro-team@latest: {}, enterprise-cloud@latest: {}}, - 'ja': { free-pro-team@latest: {}, enterprise-cloud@latest: {}} - } - */ - if (!restOperationData) { - restOperationData = {} - Object.keys(req.context.languages).forEach((language) => { - restOperationData![language] = {} - Object.keys(req.context.allVersions).forEach( - (version) => (restOperationData![language][version] = {}) - ) - }) - } - - const restOperations = rest[currentVersion][category] + const restOperations = await getRest(currentVersion, category) // The context passed will have the Markdown content for the language // of the page being requested and the Markdown will be rendered // using the `currentVersion` - if (!(category in restOperationData[currentLanguage][currentVersion])) { - restOperationData[currentLanguage][currentVersion][category] = (await getRestOperationData( - category, - restOperations, - req.context - )) as CategoryDataT - } + const { descriptions, miniTocItems, introContent } = (await getRestOperationData( + category, + currentLanguage, + currentVersion, + req.context + )) as CategoryDataT return { props: { restOperations, mainContext: getMainContext(req, res), - descriptions: restOperationData[currentLanguage][currentVersion][category].descriptions, - miniTocItems: restOperationData[currentLanguage][currentVersion][category].miniTocItems, - introContent: restOperationData[currentLanguage][currentVersion][category].introContent, + descriptions, + miniTocItems, + introContent, }, } } diff --git a/tests/rendering/rest.js b/tests/rendering/rest.js index 8890900f0be8..25641c6a760b 100644 --- a/tests/rendering/rest.js +++ b/tests/rendering/rest.js @@ -1,62 +1,19 @@ -import { fileURLToPath } from 'url' -import path from 'path' -import fs from 'fs/promises' -import { difference } from 'lodash-es' import { getDOM } from '../helpers/supertest.js' import getRest, { getEnabledForApps } from '../../lib/rest/index.js' import { jest } from '@jest/globals' import { allVersions } from '../../lib/all-versions.js' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -// list of REST markdown files that do not correspond to REST API resources -// TODO could we get this list dynamically, say via page frontmatter? -const excludeFromResourceNameCheck = [ - 'endpoints-available-for-github-apps.md', - 'permissions-required-for-github-apps.md', - 'index.md', -] - describe('REST references docs', () => { jest.setTimeout(3 * 60 * 1000) - let operations - - beforeAll(async () => { - operations = await getRest() - }) - - test('markdown file exists for every operationId prefix in the api.github.com schema', async () => { - const categories = [ - ...new Set( - Object.values(operations) - .map((version) => Object.keys(version)) - .flat() - ), - ] - const referenceDir = path.join(__dirname, '../../content/rest/reference') - const filenames = (await fs.readdir(referenceDir)) - .filter( - (filename) => - !excludeFromResourceNameCheck.find((excludedFile) => filename.endsWith(excludedFile)) - ) - .map((filename) => filename.replace('.md', '')) - - const missingResource = - 'Found a markdown file in content/rest/reference that is not represented by an OpenAPI REST operation category.' - expect(difference(filenames, categories), missingResource).toEqual([]) - - const missingFile = - 'Found an OpenAPI REST operation category that is not represented by a markdown file in content/rest/reference.' - expect(difference(categories, filenames), missingFile).toEqual([]) - }) - test('loads schema data for all versions', async () => { for (const version in allVersions) { + const checksRestOperations = getRest(version, 'checks') const $ = await getDOM(`/en/${version}/rest/reference/checks`) const domH3Ids = $('h3') .map((i, h3) => $(h3).attr('id')) .get() - const schemaSlugs = Object.values(operations[version].checks) + const schemaSlugs = Object.values(checksRestOperations) .flat() .map((operation) => operation.slug) expect(schemaSlugs.every((slug) => domH3Ids.includes(slug))).toBe(true) @@ -80,9 +37,4 @@ describe('REST references docs', () => { expect(schemaSlugs.every((slug) => domH3Ids.includes(slug))).toBe(true) } }) - - test('no wrongly detected AppleScript syntax highlighting in schema data', async () => { - const operations = await getRest() - expect(JSON.stringify(operations).includes('hljs language-applescript')).toBe(false) - }) }) diff --git a/tests/unit/openapi-schema.js b/tests/unit/openapi-schema.js index ce24e2fd626b..465cee2cab3d 100644 --- a/tests/unit/openapi-schema.js +++ b/tests/unit/openapi-schema.js @@ -1,31 +1,73 @@ +import fs from 'fs/promises' import { fileURLToPath } from 'url' import path from 'path' + +import dedent from 'dedent' +import { describe } from '@jest/globals' import walk from 'walk-sync' -import { get, isPlainObject } from 'lodash-es' +import { get, isPlainObject, difference } from 'lodash-es' + import { allVersions } from '../../lib/all-versions.js' -import getRest, { getFlatListOfOperations } from '../../lib/rest/index.js' -import dedent from 'dedent' -import { beforeAll } from '@jest/globals' +import getRest from '../../lib/rest/index.js' + const __dirname = path.dirname(fileURLToPath(import.meta.url)) const schemasPath = path.join(__dirname, '../../lib/rest/static/decorated') -let operations = null -let allOperations = null -beforeAll(async () => { - operations = await getRest() - allOperations = getFlatListOfOperations(operations) -}) +async function getFlatListOfOperations(version) { + const flatList = [] + const operations = await getRest(version) + + for (const category of Object.keys(operations)) { + const subcategories = Object.keys(operations[category]) + for (const subcategory of subcategories) { + flatList.push(...operations[category][subcategory]) + } + } + return flatList +} -describe('OpenAPI schema validation', () => { - test('makes an object', () => { - expect(isPlainObject(operations)).toBe(true) +describe('markdown for each rest version', () => { + test('markdown file exists for every operationId prefix in all versions of the OpenAPI schema', async () => { + // list of REST markdown files that do not correspond to REST API resources + // TODO could we get this list dynamically, say via page frontmatter? + const excludeFromResourceNameCheck = [ + 'endpoints-available-for-github-apps.md', + 'permissions-required-for-github-apps.md', + 'index.md', + ] + + // Unique set of all categories across all versions of the OpenAPI schema + const allCategories = new Set() + + for (const version in allVersions) { + const restOperations = await getRest(version) + Object.keys(restOperations).forEach((category) => allCategories.add(category)) + } + + const referenceDir = path.join(__dirname, '../../content/rest/reference') + const filenames = (await fs.readdir(referenceDir)) + .filter( + (filename) => + !excludeFromResourceNameCheck.find((excludedFile) => filename.endsWith(excludedFile)) + ) + .map((filename) => filename.replace('.md', '')) + + const missingResource = + 'Found a markdown file in content/rest/reference that is not represented by an OpenAPI REST operation category.' + expect(difference(filenames, [...allCategories]), missingResource).toEqual([]) + + const missingFile = + 'Found an OpenAPI REST operation category that is not represented by a markdown file in content/rest/reference.' + expect(difference([...allCategories], filenames), missingFile).toEqual([]) }) +}) +describe('OpenAPI schema validation', () => { // ensure every version defined in allVersions has a correlating static // decorated file, while allowing decorated files to exist when a version // is not yet defined in allVersions (e.g., a GHEC static file can exist // even though the version is not yet supported in the docs) - test('every OpenAPI version must have a schema file in the docs', () => { + test('every OpenAPI version must have a schema file in the docs', async () => { const decoratedFilenames = walk(schemasPath).map((filename) => path.basename(filename, '.json')) Object.values(allVersions) @@ -35,95 +77,122 @@ describe('OpenAPI schema validation', () => { }) }) - test('operations object structure organized by version, category, and subcategory', () => { - expect(allOperations.every((operation) => operation.verb)).toBe(true) + // remove? + test('operations object structure organized by version, category, and subcategory', async () => { + for (const version in allVersions) { + const operations = await getFlatListOfOperations(version) + expect(operations.every((operation) => operation.verb)).toBe(true) + } }) - test('number of openapi versions', () => { - const schemaVersions = Object.keys(operations) - // there are at least 5 versions available (3 ghes [when a version - // has been deprecated], api.github.com, enterprise-cloud, and github.ae) - expect(schemaVersions.length).toBeGreaterThanOrEqual(6) + test('no wrongly detected AppleScript syntax highlighting in schema data', async () => { + expect.assertions(Object.keys(allVersions).length) + await Promise.all( + Object.keys(allVersions).map(async (version) => { + const operations = await getRest(version) + expect(JSON.stringify(operations).includes('hljs language-applescript')).toBe(false) + }) + ) }) }) -function findOperation(method, path) { +async function findOperation(version, method, path) { + const allOperations = await getFlatListOfOperations(version) return allOperations.find((operation) => { return operation.requestPath === path && operation.verb.toLowerCase() === method.toLowerCase() }) } describe('x-codeSamples for curl', () => { - test('GET', () => { - const operation = findOperation('GET', '/repos/{owner}/{repo}') - expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'Shell') - const expected = - 'curl \\\n' + - ' -H "Accept: application/vnd.github.v3+json" \\\n' + - ' https://api.github.com/repos/octocat/hello-world' - expect(source).toEqual(expected) + test('GET', async () => { + for (const version in allVersions) { + let domain = 'https://api.github.com' + if (version.includes('enterprise-server')) { + domain = 'http(s)://{hostname}/api/v3' + } else if (version === 'github-ae@latest') { + domain = 'https://{hostname}/api/v3' + } + const operation = await findOperation(version, 'GET', '/repos/{owner}/{repo}') + expect(isPlainObject(operation)).toBe(true) + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'Shell') + const expected = + 'curl \\\n' + + ' -H "Accept: application/vnd.github.v3+json" \\\n' + + ` ${domain}/repos/octocat/hello-world` + expect(source).toEqual(expected) + } }) - test('operations with required preview headers match Shell examples', () => { - const operationsWithRequiredPreviewHeaders = allOperations.filter((operation) => { - const previews = get(operation, 'x-github.previews', []) - return previews.some((preview) => preview.required) - }) - - const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter( - (operation) => { - const { source: codeSample } = operation['x-codeSamples'].find( - (sample) => sample.lang === 'Shell' - ) - return ( - codeSample.includes('-H "Accept: application/vnd.github') && - !codeSample.includes('application/vnd.github.v3+json') - ) - } - ) - expect(operationsWithRequiredPreviewHeaders.length).toEqual( - operationsWithHeadersInCodeSample.length - ) + test('operations with required preview headers match Shell examples', async () => { + for (const version in allVersions) { + const allOperations = await getFlatListOfOperations(version) + const operationsWithRequiredPreviewHeaders = allOperations.filter((operation) => { + const previews = get(operation, 'x-github.previews', []) + return previews.some((preview) => preview.required) + }) + + const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter( + (operation) => { + const { source: codeSample } = operation['x-codeSamples'].find( + (sample) => sample.lang === 'Shell' + ) + return ( + codeSample.includes('-H "Accept: application/vnd.github') && + !codeSample.includes('application/vnd.github.v3+json') + ) + } + ) + expect(operationsWithRequiredPreviewHeaders.length).toEqual( + operationsWithHeadersInCodeSample.length + ) + } }) }) describe('x-codeSamples for @octokit/core.js', () => { - test('GET', () => { - const operation = findOperation('GET', '/repos/{owner}/{repo}') - expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') - const plainText = source.replace(/<[^>]+>/g, '').trim() - const expected = dedent`await octokit.request('GET /repos/{owner}/{repo}', { - owner: 'octocat', - repo: 'hello-world' - })` - expect(plainText).toEqual(expected) + test('GET', async () => { + for (const version in allVersions) { + const operation = await findOperation(version, 'GET', '/repos/{owner}/{repo}') + expect(isPlainObject(operation)).toBe(true) + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') + const plainText = source.replace(/<[^>]+>/g, '').trim() + const expected = dedent`await octokit.request('GET /repos/{owner}/{repo}', { + owner: 'octocat', + repo: 'hello-world' + })` + expect(plainText).toEqual(expected) + } }) - test('POST', () => { - const operation = findOperation('POST', '/repos/{owner}/{repo}/git/trees') - expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') - const plainText = source.replace(/<[^>]+>/g, '').trim() - const expected = dedent`await octokit.request('POST /repos/{owner}/{repo}/git/trees', { - owner: 'octocat', - repo: 'hello-world', - tree: [ - { - path: 'path', - mode: 'mode', - type: 'type', - sha: 'sha', - content: 'content' - } - ] - })` - expect(plainText).toEqual(expected) + test('POST', async () => { + for (const version in allVersions) { + const operation = await findOperation(version, 'POST', '/repos/{owner}/{repo}/git/trees') + expect(isPlainObject(operation)).toBe(true) + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') + const plainText = source.replace(/<[^>]+>/g, '').trim() + const expected = dedent`await octokit.request('POST /repos/{owner}/{repo}/git/trees', { + owner: 'octocat', + repo: 'hello-world', + tree: [ + { + path: 'path', + mode: 'mode', + type: 'type', + sha: 'sha', + content: 'content' + } + ] + })` + expect(plainText).toEqual(expected) + } }) - test('PUT', () => { - const operation = findOperation('PUT', '/authorizations/clients/{client_id}/{fingerprint}') + test('PUT', async () => { + const operation = await findOperation( + 'free-pro-team@latest', + 'PUT', + '/authorizations/clients/{client_id}/{fingerprint}' + ) expect(isPlainObject(operation)).toBe(true) const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') const plainText = source.replace(/<[^>]+>/g, '').trim() @@ -135,45 +204,50 @@ describe('x-codeSamples for @octokit/core.js', () => { expect(plainText).toEqual(expected) }) - test('operations with required preview headers match JavaScript examples', () => { - const operationsWithRequiredPreviewHeaders = allOperations.filter((operation) => { - const previews = get(operation, 'x-github.previews', []) - return previews.some((preview) => preview.required) - }) - - // Find something that looks like the following in each code sample: - /* - mediaType: { - previews: [ - 'machine-man' - ] - } - */ - const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter( - (operation) => { - const { source: codeSample } = operation['x-codeSamples'].find( - (sample) => sample.lang === 'JavaScript' - ) - return codeSample.match(/mediaType: \{\s+previews: /g) - } - ) - expect(operationsWithRequiredPreviewHeaders.length).toEqual( - operationsWithHeadersInCodeSample.length - ) + test('operations with required preview headers match JavaScript examples', async () => { + for (const version in allVersions) { + const allOperations = await getFlatListOfOperations(version) + const operationsWithRequiredPreviewHeaders = allOperations.filter((operation) => { + const previews = get(operation, 'x-github.previews', []) + return previews.some((preview) => preview.required) + }) + + // Find something that looks like the following in each code sample: + /* + mediaType: { + previews: [ + 'machine-man' + ] + } + */ + const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter( + (operation) => { + const { source: codeSample } = operation['x-codeSamples'].find( + (sample) => sample.lang === 'JavaScript' + ) + return codeSample.match(/mediaType: \{\s+previews: /g) + } + ) + expect(operationsWithRequiredPreviewHeaders.length).toEqual( + operationsWithHeadersInCodeSample.length + ) + } }) // skipped because the definition is current missing the `content-type` parameter // GitHub GitHub issue: 155943 - test.skip('operation with content-type parameter', () => { - const operation = findOperation('POST', '/markdown/raw') - expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') - const expected = dedent`await octokit.request('POST /markdown/raw', { - data: 'data', - headers: { - 'content-type': 'text/plain; charset=utf-8' - } - })` - expect(source).toEqual(expected) + test.skip('operation with content-type parameter', async () => { + for (const version in allVersions) { + const operation = await findOperation(version, 'POST', '/markdown/raw') + expect(isPlainObject(operation)).toBe(true) + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') + const expected = dedent`await octokit.request('POST /markdown/raw', { + data: 'data', + headers: { + 'content-type': 'text/plain; charset=utf-8' + } + })` + expect(source).toEqual(expected) + } }) })