diff --git a/e2e/index.ts b/e2e/index.ts index e27b40d647..2c348bcfd2 100644 --- a/e2e/index.ts +++ b/e2e/index.ts @@ -7,6 +7,7 @@ import { dockerDeployment } from './tests/docker' import { DeploymentConfiguration } from './types' import { env, getCommitId } from './utils' import { vercelDeployment } from './tests/vercel' +import { netlifyDeployment } from './tests/netlify-edge' const AVAILABLE_TEST_PLANS = { 'cf-worker': cloudFlareDeployment, @@ -15,6 +16,7 @@ const AVAILABLE_TEST_PLANS = { 'aws-lambda': awsLambdaDeployment, 'vercel-function': vercelDeployment, 'docker-node-17': dockerDeployment('node:17.8.0-alpine3.14'), + 'netlify-edge': netlifyDeployment, } async function main() { diff --git a/e2e/package.json b/e2e/package.json index 525ab9ddce..69dd22c8a9 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -4,14 +4,15 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@pulumi/pulumi": "3.40.1", - "@pulumi/cloudflare": "4.12.1", - "@pulumi/azure-native": "1.78.0", - "@pulumi/awsx": "0.40.1", "@pulumi/aws": "5.15.0", + "@pulumi/awsx": "0.40.1", + "@pulumi/azure-native": "1.78.0", + "@pulumi/cloudflare": "4.12.1", "@pulumi/docker": "3.4.1", + "@pulumi/pulumi": "3.40.1", "@types/node": "16.11.60", "typescript": "4.8.4", + "ts-node": "10.9.1", "ts-node": "10.9.1" }, "scripts": { diff --git a/e2e/tests/netlify-edge.ts b/e2e/tests/netlify-edge.ts new file mode 100644 index 0000000000..1cc94a911b --- /dev/null +++ b/e2e/tests/netlify-edge.ts @@ -0,0 +1,159 @@ +import * as pulumi from '@pulumi/pulumi' +import { + assertGraphiQL, + assertQuery, + env, + execPromise, + fsPromises, + waitForEndpoint, +} from '../utils' +import { DeploymentConfiguration } from '../types' + +type ProviderInputs = { + name: string + files: { + file: string + data: string + }[] +} + +type NetlifyDeploymentInputs = { + [K in keyof ProviderInputs]: pulumi.Input +} + +class NetlifyProvider implements pulumi.dynamic.ResourceProvider { + private baseUrl = 'https://api.netlify.com/api/' + private authToken = env('NETLIFY_AUTH_TOKEN') + + async delete(id: string) { + const response = await fetch(`${this.baseUrl}/v1/sites/${id}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${this.authToken}`, + }, + }) + + // https://open-api.netlify.com/#tag/site/operation/deleteSite + if (response.status !== 204) { + throw new Error( + `Failed to delete Netlify site deployment ${id}: invalid status code (${ + response.status + }), body: ${await response.text()}`, + ) + } + } + + async create(inputs: ProviderInputs): Promise { + // First we create a site + const createSiteResponse = await fetch(`${this.baseUrl}/v1/sites`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${this.authToken}`, + }, + body: JSON.stringify({ + name: inputs.name, + }), + }) + + // https://open-api.netlify.com/#tag/site/operation/createSite + if (createSiteResponse.status !== 201) { + throw new Error( + `Failed to create Netlify deployment: invalid status code (${ + createSiteResponse.status + }), body: ${await createSiteResponse.text()}`, + ) + } + + const createResponseJson = await createSiteResponse.json() + const siteId = createResponseJson.id + + const createDeploymentResponse = await fetch( + `${this.baseUrl}/v1/sites/${siteId}/deploys`, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${this.authToken}`, + }, + body: JSON.stringify({ + files: inputs.files, + }), + }, + ) + + // https://open-api.netlify.com/#tag/deploy/operation/createSiteDeploy + if (createDeploymentResponse.status !== 200) { + throw new Error( + `Failed to create Netlify deployment: invalid status code (${ + createDeploymentResponse.status + }), body: ${await createDeploymentResponse.text()}`, + ) + } + + const createDeploymentResponseJson = await createDeploymentResponse.json() + + return { + id: createDeploymentResponseJson['site_id'], + outs: { + url: createDeploymentResponseJson['deploy_ssl_url'], + }, + } + } +} + +export class NetlifyDeployment extends pulumi.dynamic.Resource { + public readonly url: pulumi.Output + + constructor( + name: string, + props: NetlifyDeploymentInputs, + opts?: pulumi.CustomResourceOptions, + ) { + super( + new NetlifyProvider(), + name, + { + url: undefined, + ...props, + }, + opts, + ) + } +} + +export const netlifyDeployment: DeploymentConfiguration<{ + functionUrl: string +}> = { + prerequisites: async () => { + // Build and bundle the function + console.info('\t\tℹ️ Bundling the Netlify Function....') + await execPromise('yarn build', { + cwd: '../examples/netlify-edge', + }) + }, + program: async () => { + const deployment = new NetlifyDeployment('netlify-function', { + files: [ + // { + // file: '/api/graphql.js', + // data: await fsPromises.readFile( + // '../examples/nextjs/dist/index.js', + // 'utf-8', + // ), + // }, + ], + name: `yoga-e2e-testing`, + }) + + return { + functionUrl: pulumi.interpolate`${deployment.url}/api/graphql`, + } + }, + test: async ({ functionUrl }) => { + console.log(`ℹ️ Netlify Function deployed to URL: ${functionUrl.value}`) + // await waitForEndpoint(functionUrl.value, 5, 10000) + // await assertGraphiQL(functionUrl.value) + // await assertQuery(functionUrl.value) + }, +}