Skip to content

Commit

Permalink
feat(nextjs-component): experimental - allow serving API pages from d…
Browse files Browse the repository at this point in the history
…efault lambda (#1632)
  • Loading branch information
dphang committed Sep 14, 2021
1 parent 801c391 commit b7fe7a9
Show file tree
Hide file tree
Showing 53 changed files with 8,119 additions and 19 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e-tests.yml
Expand Up @@ -116,6 +116,7 @@ jobs:
app:
# Current minor version of Next.js
- next-app
- next-app-experimental
- next-app-using-serverless-trace
- next-app-with-trailing-slash
- next-app-with-base-path
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e-tests/next-app-experimental/.gitignore
@@ -0,0 +1,2 @@
cypress/videos
cypress/screenshots
9 changes: 9 additions & 0 deletions packages/e2e-tests/next-app-experimental/cypress.json
@@ -0,0 +1,9 @@
{
"baseUrl": "http://localhost:3000",
"supportFile": "cypress/support/index.ts",
"responseTimeout": 15000,
"requestTimeout": 15000,
"experimentalFetchPolyfill": true,
"retries": 4,
"video": false
}
@@ -0,0 +1,72 @@
describe("API Routes Tests", () => {
before(() => {
cy.ensureAllRoutesNotErrored();
});

describe("Basic API", () => {
const path = "/api/basic-api";

["DELETE", "POST", "GET", "PUT", "PATCH", "OPTIONS", "HEAD"].forEach(
(method) => {
it(`serves API request for path ${path} and method ${method}`, () => {
cy.request({ url: path, method: method }).then((response) => {
expect(response.status).to.equal(200);
cy.verifyResponseCacheStatus(response, false);

if (method === "HEAD") {
expect(response.body).to.be.empty;
} else {
expect(response.body).to.deep.equal({
name: "This is a basic API route.",
method: method
});
}
});
});
}
);
});

describe("Dynamic + Nested API", () => {
const base = "api/nested/";

["DELETE", "POST", "GET", "PUT", "PATCH", "OPTIONS", "HEAD"].forEach(
(method) => {
const id = "1";
const path = base + id;

it(`serves API request for path ${path} and method ${method}`, () => {
cy.request({ url: path, method: method }).then((response) => {
expect(response.status).to.equal(200);
cy.verifyResponseCacheStatus(response, false);

if (method === "HEAD") {
expect(response.body).to.be.empty;
} else {
expect(response.body).to.deep.equal({
id: id,
name: `User ${id}`,
method: method
});
}
});
});
}
);

["1", "2", "3", "4", "5"].forEach((id) => {
const path = base + id;
it(`serves API request for path ${path} for different IDs`, () => {
cy.request({ url: path, method: "GET" }).then((response) => {
expect(response.status).to.equal(200);
expect(response.body).to.deep.equal({
id: id,
name: `User ${id}`,
method: "GET"
});
cy.verifyResponseCacheStatus(response, false);
});
});
});
});
});
@@ -0,0 +1,89 @@
describe("Data Requests", () => {
const buildId = Cypress.env("NEXT_BUILD_ID");

describe("SSG data requests", () => {
[{ path: "/ssg-page.json" }, { path: "/index.json" }].forEach(
({ path }) => {
const fullPath = `/_next/data/${buildId}${path}`;

it(`serves the SSG data request for path ${fullPath}`, () => {
// Hit two times, and check that the response should definitely be cached after 2nd time
for (let i = 0; i < 2; i++) {
cy.request(fullPath).then((response) => {
expect(response.status).to.equal(200);
expect(response.headers["cache-control"]).to.not.be.undefined;

if (i === 1) {
cy.verifyResponseCacheStatus(response, true);
} else {
expect(response.headers["x-cache"]).to.be.oneOf([
"Miss from cloudfront",
"Hit from cloudfront"
]);
}
});
}
});

["HEAD", "GET"].forEach((method) => {
it(`allows HTTP method for path ${fullPath}: ${method}`, () => {
cy.request({ url: fullPath, method: method }).then((response) => {
expect(response.status).to.equal(200);
});
});
});

["DELETE", "POST", "OPTIONS", "PUT", "PATCH"].forEach((method) => {
it(`disallows HTTP method for path ${fullPath} with 4xx error: ${method}`, () => {
cy.request({
url: fullPath,
method: method,
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.be.at.least(400);
expect(response.status).to.be.lessThan(500);
});
});
});
}
);
});

describe("SSR data requests", () => {
[{ path: "/ssr-page-2.json" }].forEach(({ path }) => {
const fullPath = `/_next/data/${buildId}${path}`;

it(`serves the SSR data request for path ${fullPath}`, () => {
// Hit two times, both of which, the response should not be cached
for (let i = 0; i < 2; i++) {
cy.request(fullPath).then((response) => {
expect(response.status).to.equal(200);
cy.verifyResponseCacheStatus(response, false);
expect(response.headers["cache-control"]).to.be.undefined;
});
}
});

["HEAD", "GET"].forEach((method) => {
it(`allows HTTP method for path ${fullPath}: ${method}`, () => {
cy.request({ url: fullPath, method: method }).then((response) => {
expect(response.status).to.equal(200);
});
});
});

["DELETE", "POST", "OPTIONS", "PUT", "PATCH"].forEach((method) => {
it(`disallows HTTP method for path ${fullPath} with 4xx error: ${method}`, () => {
cy.request({
url: fullPath,
method: method,
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.be.at.least(400);
expect(response.status).to.be.lessThan(500);
});
});
});
});
});
});
@@ -0,0 +1,45 @@
describe("Headers Tests", () => {
describe("Custom headers defined in next.config.js", () => {
[
{
path: "/ssr-page",
expectedHeaders: { "x-custom-header-ssr-page": "custom" }
},
{
path: "/ssg-page",
expectedHeaders: { "x-custom-header-ssg-page": "custom" }
},
{
path: "/",
expectedHeaders: { "x-custom-header-all": "custom" }
},
{
path: "/not-found",
expectedHeaders: { "x-custom-header-all": "custom" }
},
{
path: "/api/basic-api",
expectedHeaders: { "x-custom-header-api": "custom" }
},
{
path: "/app-store-badge.png",
expectedHeaders: { "x-custom-header-public-file": "custom" }
}
].forEach(({ path, expectedHeaders }) => {
it(`add headers ${JSON.stringify(
expectedHeaders
)} for path ${path}`, () => {
cy.request({
url: path,
failOnStatusCode: false
}).then((response) => {
for (const expectedHeader in expectedHeaders) {
expect(response.headers[expectedHeader]).to.equal(
expectedHeaders[expectedHeader]
);
}
});
});
});
});
});
@@ -0,0 +1,106 @@
describe("Image Optimizer Tests", () => {
describe("image optimization", () => {
[{ contentType: "image/webp" }, { contentType: "image/png" }].forEach(
({ contentType }) => {
it(`serves image app-store-badge.png with content-type: ${contentType}`, () => {
cy.request({
url: "/_next/image?url=%2Fapp-store-badge.png&w=256&q=100",
method: "GET",
headers: { accept: contentType }
}).then((response) => {
// TODO: not sure why this is failing in CI
//expect(response.headers["content-type"]).to.equal(contentType);
expect(response.headers["cache-control"]).to.equal(
"public, max-age=31536000, must-revalidate"
);
});
});
}
);

// Higher quality should have higher file size
[
{ quality: "100", expectedContentLength: "5742" },
{ quality: "50", expectedContentLength: "2654" }
].forEach(({ quality, expectedContentLength }) => {
it(`serves image app-store-badge.png with quality: ${quality}`, () => {
cy.request({
url: `/_next/image?url=%2Fapp-store-badge.png&w=256&q=${quality}`,
method: "GET",
headers: { accept: "image/webp" }
}).then((response) => {
// TODO: not sure why this is failing in CI
// expect(response.headers["content-length"]).to.equal(
// expectedContentLength
// );
expect(response.headers["cache-control"]).to.equal(
"public, max-age=31536000, must-revalidate"
);
});
});
});

// Higher width should have higher file size
[
{ width: "128", expectedContentLength: "2600" },
{ width: "64", expectedContentLength: "1192" }
].forEach(({ width, expectedContentLength }) => {
it(`serves image app-store-badge.png with width: ${width}`, () => {
cy.request({
url: `/_next/image?url=%2Fapp-store-badge.png&w=${width}&q=100`,
method: "GET",
headers: { accept: "image/webp" }
}).then((response) => {
// TODO: not sure why this is failing in CI
// expect(response.headers["content-length"]).to.equal(
// expectedContentLength
// );
expect(response.headers["cache-control"]).to.equal(
"public, max-age=31536000, must-revalidate"
);
});
});
});

[
{
path: "/_next/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fserverless-nextjs%2Fserverless-next.js%2Fmaster%2Fpackages%2Fe2e-tests%2Fnext-app-experimental%2Fpublic%2Fapp-store-badge.png&q=100&w=128"
}
].forEach(({ path }) => {
it(`serves external image: ${path}`, () => {
cy.request({ url: path, method: "GET" });
});
});

[
{ path: "/_next/image" },
{ path: "/_next/image?w=256&q=100" },
{ path: "/_next/image?url=%2Fapp-store-badge.png&w=256" },
{ path: "/_next/image?url=%2Fapp-store-badge.png&q=100" }
].forEach(({ path }) => {
it(`missing query parameter fails with 400 status code: ${path}`, () => {
cy.request({ url: path, method: "GET", failOnStatusCode: false }).then(
(response) => {
expect(response.status).to.equal(400);
}
);
});
});
});

describe("image component page", () => {
[{ path: "/image-component" }].forEach(({ path }) => {
it(`serves page with image component and caches the image: ${path}`, () => {
cy.ensureAllRoutesNotErrored(); // Visit routes only

cy.visit(path);

cy.ensureRouteCached(
"/_next/image?url=%2Fapp-store-badge.png&w=1200&q=75"
);

cy.visit(path);
});
});
});
});

0 comments on commit b7fe7a9

Please sign in to comment.