frontitygit
Version:
A Frontity source package for the REST API of self-hosted and WordPress.com sites
701 lines (605 loc) • 23 kB
text/typescript
/* eslint-disable @typescript-eslint/no-namespace */
import * as frontity from "frontity";
import clone from "clone-deep";
import { InitializedStore, createStore } from "@frontity/connect";
import { Pattern, Handler, Packages } from "../../types";
import wpSource from "..";
import { ServerError } from "@frontity/source";
import { RedirectionData } from "@frontity/source/types/data";
import { Headers } from "node-fetch";
let handler: jest.Mocked<Pattern<Handler>>;
let store: InitializedStore<Packages>;
let mockedFetch: jest.MockedFunction<typeof fetch>;
declare global {
namespace jest {
interface Matchers<R> {
toHaveBeenCalledBefore(func: any): R;
}
}
}
/**
* Add a `.toHaveBeenCalledBefore()` matcher to jest to make the tests more
* clear.
*/
expect.extend({
toHaveBeenCalledBefore(first, second) {
if (
first.mock.invocationCallOrder[0] < second.mock.invocationCallOrder[0]
) {
return {
message: () => `expected ${first} to have been called before ${second}`,
pass: true,
};
} else {
return {
message: () => `expected ${first} to have been called before ${second}`,
pass: false,
};
}
},
});
const serverFetchParams = [
"https://wp.domain.com/some-post/",
{
method: "HEAD",
redirect: "manual",
},
];
const serverResponse = {
status: 301,
headers: new Headers({
location: "https://wp.domain.com/redirected-url",
}),
};
const clientFetchParams = [
"https://wp.domain.com/some-post/",
{
method: "HEAD",
},
];
const clientResponse = {
url: "https://wp.domain.com/redirected-url",
redirected: true,
status: 200,
};
describe.each`
platform | fetchRedirectionParams | response
${"server"} | ${serverFetchParams} | ${serverResponse}
${"client"} | ${clientFetchParams} | ${clientResponse}
`("Redirection tests", ({ platform, fetchRedirectionParams, response }) => {
beforeEach(() => {
mockedFetch = jest.fn((_) =>
Promise.resolve((response as unknown) as Response)
);
(frontity.fetch as typeof fetch) = mockedFetch;
// Initialize the store.
store = createStore<Packages>(clone(wpSource()));
store.state.source.url = "https://wp.domain.com";
store.state.frontity = {
platform,
url: "https://www.domain.com/",
};
handler = {
name: "always",
priority: 10,
pattern: "/(.*)",
func: jest.fn(async ({ link, state }) => {
await Promise.resolve();
Object.assign(state.source.data[link], {
type: "example",
id: 1,
isPostType: true,
});
}),
};
store.libraries.source.handlers.push(handler);
});
describe("redirections: all", () => {
it(`${platform}: Should handle a redirect with 'all'`, async () => {
store.state.source.redirections = "all";
await store.actions.source.fetch("/some-post/");
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).not.toHaveBeenCalled();
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
});
describe("redirections: 404", () => {
beforeEach(() => {
// The handler throws a ServerError to imitate getting a 404 from the WP
// REST API.
handler.func = jest.fn(() => {
throw new ServerError("There was an error", 404);
}) as any;
});
it(`${platform}: Should ignore the redirection if there is no error`, async () => {
store.state.source.redirections = "404";
// Restore the handler to the original implementation.
handler.func.mockRestore();
await store.actions.source.fetch("/some-post/");
expect(mockedFetch).not.toHaveBeenCalled();
expect(handler.func).toHaveBeenCalledTimes(1);
});
it(`${platform}: Should NOT redirect on an error that is not a 404`, async () => {
store.state.source.redirections = "404";
// Throw a 500 inside of the handler.
handler.func = jest.fn(() => {
throw new ServerError("There was an error", 500);
}) as any;
await store.actions.source.fetch("/some-post/");
expect(mockedFetch).not.toHaveBeenCalled();
expect(handler.func).toHaveBeenCalledTimes(1);
});
it(`${platform}: Should redirect on 404`, async () => {
store.state.source.redirections = "404";
await store.actions.source.fetch("/some-post/");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).toHaveBeenCalledTimes(1);
expect(handler.func).toHaveBeenCalledBefore(mockedFetch);
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should redirect on 404 when the redirection is a 302 redirection`, async () => {
store.state.source.redirections = "404";
if (platform === "server") {
mockedFetch = jest.fn((_) =>
Promise.resolve(({
...serverResponse,
status: 302,
} as unknown) as Response)
);
}
(frontity.fetch as typeof fetch) = mockedFetch;
await store.actions.source.fetch("/some-post/");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).toHaveBeenCalledTimes(1);
expect(handler.func).toHaveBeenCalledBefore(mockedFetch);
const snapshot = `
Object {
"/some-post/": Object {
"is30${platform === "server" ? "2" : "1"}": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 30${platform === "server" ? "2" : "1"},
"route": "/some-post/",
},
}
`;
const is302 = platform === "server";
const is301 = platform === "client";
const data = store.state.source.data["/some-post/"] as RedirectionData;
expect(!!data.is302).toBe(is302);
expect(!!data.is301).toBe(is301);
expect(store.state.source.data).toMatchInlineSnapshot(snapshot);
});
it(`${platform}: Should return a normal 404 error if fetching the redirection fails`, async () => {
store.state.source.redirections = "404";
// Fetching the redirection fails.
mockedFetch = jest.fn().mockRejectedValueOnce("Fetch Error");
(frontity.fetch as typeof fetch) = mockedFetch;
await store.actions.source.fetch("/some-post/");
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"errorStatus": 404,
"errorStatusText": "There was an error",
"is404": true,
"isError": true,
"isFetching": false,
"isReady": true,
"link": "/some-post/",
"page": 1,
"query": Object {},
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should handle redirections if the URL contains a query string`, async () => {
store.state.source.redirections = "404";
await store.actions.source.fetch("/some-post?key=value&key2=value2");
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/?key=value&key2=value2": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/?key=value&key2=value2",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {
"key": "value",
"key2": "value2",
},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
});
describe("redirections: RegExp and RegExp[]", () => {
beforeEach(() => {
// The handler throws a 404.
handler.func = jest.fn(() => {
throw new ServerError("There was an error", 404);
}) as any;
});
it(`${platform}: Should fetch the redirection when the redirection is a single regex`, async () => {
store.state.source.redirections = "RegExp:/some-(\\w*)";
await store.actions.source.fetch("/some-post");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).not.toHaveBeenCalled();
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should match the redirection with pagination and queries`, async () => {
store.state.source.redirections = "RegExp:/some-(\\w*)";
await store.actions.source.fetch("/some-post");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(handler.func).not.toHaveBeenCalled();
store.state.source.redirections =
"RegExp:\\/category\\/some-cat\\/page\\/[2,3]";
await store.actions.source.fetch("/category/some-cat/page/2");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(2);
expect(handler.func).not.toHaveBeenCalled();
await store.actions.source.fetch("/category/some-cat/page/5");
// The `fetch()` was not called.
expect(mockedFetch).toHaveBeenCalledTimes(2);
expect(handler.func).toHaveBeenCalledTimes(1);
store.state.source.redirections = ["RegExp:(\\?|&)p=(\\d+)"];
await store.actions.source.fetch("/?p=123&preview=true");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(3);
expect(handler.func).toHaveBeenCalledTimes(1);
await store.actions.source.fetch("/?page_id=123&preview=true");
// The `fetch()` was not called.
expect(mockedFetch).toHaveBeenCalledTimes(3);
expect(handler.func).toHaveBeenCalledTimes(2);
});
it(`${platform}: Should just return a 404 if the redirection does not match the regex`, async () => {
store.state.source.redirections = "RegExp:/some-other-post";
await store.actions.source.fetch("/some-post");
// The `fetch()` was NOT called.
expect(mockedFetch).toHaveBeenCalledTimes(0);
expect(handler.func).toHaveBeenCalledTimes(1);
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"errorStatus": 404,
"errorStatusText": "There was an error",
"is404": true,
"isError": true,
"isFetching": false,
"isReady": true,
"link": "/some-post/",
"page": 1,
"query": Object {},
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should fetch the redirection when the redirection is an array of regexes`, async () => {
store.state.source.redirections = [
"RegExp:/some-(.*)",
"RegExp:/some-other-post",
];
await store.actions.source.fetch("/some-post");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).not.toHaveBeenCalled();
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should fetch the redirection when the redirection is an array of regexes that includes a 404 AND the regex does NOT match the route`, async () => {
store.state.source.redirections = [
"404",
"RegExp:/some-other-post", // This regex does not match
];
await store.actions.source.fetch("/some-post");
// The `fetch()` was called.
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).toHaveBeenCalledTimes(1);
// The handler was called before the fetch because there was no match for
// the redirection regex so we only fetch the redirection once the error is thrown.
expect(handler.func).toHaveBeenCalledBefore(mockedFetch);
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should fetch the redirection when the redirection is an array of regexes that includes a 404 AND the regex matches a route`, async () => {
store.state.source.redirections = [
"404",
"RegExp:/some-(\\w*)", // This regex DOES match
];
await store.actions.source.fetch("/some-post");
// We only call the fetch once, even though we include the
expect(mockedFetch).toHaveBeenCalledTimes(1);
expect(mockedFetch).toHaveBeenCalledWith(...fetchRedirectionParams);
expect(handler.func).not.toHaveBeenCalled();
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
});
describe("redirections: external", () => {
it(`${platform}: Should not mark the Frontity URLs as external redirections`, async () => {
store.state.source.redirections = "all";
if (platform === "server") {
mockedFetch = jest.fn((_) =>
Promise.resolve(({
status: 301,
headers: new Headers({
location: "https://www.domain.com/redirected-url",
}),
} as unknown) as Response)
);
} else {
mockedFetch = jest.fn((_) =>
Promise.resolve({
url: "https://www.domain.com/redirected-url",
redirected: true,
status: 200,
} as Response)
);
}
(frontity.fetch as typeof fetch) = mockedFetch;
await store.actions.source.fetch("/some-post/");
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://www.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should not mark backend URLs as external redirections`, async () => {
store.state.source.redirections = "all";
if (platform === "server") {
mockedFetch = jest.fn((_) =>
Promise.resolve(({
status: 301,
headers: new Headers({
location: "https://wp.domain.com/redirected-url",
}),
} as unknown) as Response)
);
} else {
mockedFetch = jest.fn((_) =>
Promise.resolve({
url: "https://wp.domain.com/redirected-url",
redirected: true,
status: 200,
} as Response)
);
}
(frontity.fetch as typeof fetch) = mockedFetch;
await store.actions.source.fetch("/some-post/");
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": false,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://wp.domain.com/redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should mark external redirections`, async () => {
store.state.source.redirections = "all";
if (platform === "server") {
mockedFetch = jest.fn((_) =>
Promise.resolve(({
status: 301,
headers: new Headers({
location: "https://external-domain.com/external-redirected-url",
}),
} as unknown) as Response)
);
} else {
mockedFetch = jest.fn((_) =>
Promise.resolve({
url: "https://external-domain.com/external-redirected-url",
redirected: true,
status: 200,
} as Response)
);
}
(frontity.fetch as typeof fetch) = mockedFetch;
await store.actions.source.fetch("/some-post/");
expect(store.state.source.data).toMatchInlineSnapshot(`
Object {
"/some-post/": Object {
"is301": true,
"isExternal": true,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": "https://external-domain.com/external-redirected-url",
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`);
});
it(`${platform}: Should mark redirections as external if they fail due to CORS`, async () => {
store.state.source.redirections = "all";
if (platform === "server") {
mockedFetch = jest.fn((_) =>
Promise.resolve(({
status: 301,
headers: new Headers({
location: "https://external-domain.com/external-redirected-url",
}),
} as unknown) as Response)
);
} else {
mockedFetch = jest
.fn()
// Fetching the redirection fails first due to CORS.
.mockRejectedValueOnce("Fetch Error")
// Then the next fetch done with `redirect: manual` returns the
// opaqueredirect value.
.mockResolvedValueOnce({
url: "https://wp.domain.com/some-post/",
type: "opaqueredirect",
status: 0,
});
}
(frontity.fetch as typeof fetch) = mockedFetch;
await store.actions.source.fetch("/some-post/");
const snapshot = `
Object {
"/some-post/": Object {
"is301": true,
"isExternal": true,
"isFetching": false,
"isReady": true,
"isRedirection": true,
"link": "/some-post/",
"location": ${
platform === "server"
? '"https://external-domain.com/external-redirected-url"'
: '"https://wp.domain.com/some-post/"'
},
"page": 1,
"query": Object {},
"redirectionStatus": 301,
"route": "/some-post/",
},
}
`;
expect(store.state.source.data).toMatchInlineSnapshot(snapshot);
});
});
});