mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
091346dfe8
* wip * Remove obsolete snapshots * simplify * chore(test): Convert mocks to TypeScript and tighten fetch mock types Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove unneccessary patches * Migrate to msw instead of custom fetch mock * Address PR review comments - Split chained vi.useFakeTimers().setSystemTime() into separate calls. - Switch test setup to dynamic imports so EventEmitter.defaultMaxListeners assignment runs before module init (static imports were hoisted above it). - Drop redundant NODE_ENV guard in monkeyPatchSequelizeErrorsForJest; its sole caller already gates on env.isTest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
448 lines
13 KiB
TypeScript
448 lines
13 KiB
TypeScript
import type { Mock } from "vitest";
|
|
import { UnfurlResourceType } from "@shared/types";
|
|
import env from "@server/env";
|
|
import type { User } from "@server/models";
|
|
import {
|
|
buildCollection,
|
|
buildDocument,
|
|
buildShare,
|
|
buildUser,
|
|
} from "@server/test/factories";
|
|
import { getTestServer } from "@server/test/support";
|
|
import Iframely from "plugins/iframely/server/iframely";
|
|
|
|
const resolveCname = vi.hoisted(
|
|
() =>
|
|
(
|
|
input: string,
|
|
callback: (err: Error | null, addresses: string[]) => void
|
|
) => {
|
|
if (input.includes("valid.custom.domain")) {
|
|
callback(null, ["secure.outline.dev"]);
|
|
return;
|
|
}
|
|
|
|
callback(null, []);
|
|
}
|
|
);
|
|
|
|
vi.mock("node:dns", () => ({
|
|
default: {
|
|
resolveCname,
|
|
},
|
|
resolveCname,
|
|
}));
|
|
|
|
vi.mock("dns", () => ({
|
|
default: {
|
|
resolveCname,
|
|
},
|
|
resolveCname,
|
|
}));
|
|
|
|
vi.spyOn(Iframely, "requestResource").mockImplementation(() =>
|
|
Promise.resolve({})
|
|
);
|
|
|
|
const server = getTestServer();
|
|
|
|
describe("#urls.unfurl", () => {
|
|
let user: User;
|
|
beforeEach(async () => {
|
|
user = await buildUser();
|
|
});
|
|
|
|
it("should fail with status 400 bad request when url is invalid", async () => {
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "/doc/foo-bar",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(400);
|
|
expect(body.message).toEqual("url: Invalid URL");
|
|
});
|
|
|
|
it("should fail with status 400 bad request when mention url is invalid", async () => {
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "mention://1/foo/1",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(400);
|
|
expect(body.message).toEqual("url: Must be a valid url");
|
|
});
|
|
|
|
it("should fail with status 400 bad request when mention url is supplied without documentId", async () => {
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "mention://2767ba0e-ac5c-4533-b9cf-4f5fc456600e/user/34095ac1-c808-45c0-8c6e-6c554497de64",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(400);
|
|
expect(body.message).toEqual("body: documentId required");
|
|
});
|
|
|
|
it("should fail with status 404 not found when mention user does not exist", async () => {
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "mention://2767ba0e-ac5c-4533-b9cf-4f5fc456600e/user/34095ac1-c808-45c0-8c6e-6c554497de64",
|
|
documentId: "2767ba0e-ac5c-4533-b9cf-4f5fc456600e",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(404);
|
|
expect(body.message).toEqual("Mentioned user does not exist");
|
|
});
|
|
|
|
it("should fail with status 404 not found when document does not exist", async () => {
|
|
const mentionedUser = await buildUser({
|
|
teamId: user.teamId,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `mention://2767ba0e-ac5c-4533-b9cf-4f5fc456600e/user/${mentionedUser.id}`,
|
|
documentId: "2767ba0e-ac5c-4533-b9cf-4f5fc456600e",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(404);
|
|
expect(body.message).toEqual("Document does not exist");
|
|
});
|
|
|
|
it("should fail with status 403 forbidden when user is not authorized to read mentioned user info", async () => {
|
|
const mentionedUser = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `mention://2767ba0e-ac5c-4533-b9cf-4f5fc456600e/user/${mentionedUser.id}`,
|
|
documentId: document.id,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(403);
|
|
expect(body.message).toEqual("Authorization error");
|
|
});
|
|
|
|
it("should succeed with status 200 ok when valid mention url is supplied", async () => {
|
|
const mentionedUser = await buildUser({ teamId: user.teamId });
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `mention://2767ba0e-ac5c-4533-b9cf-4f5fc456600e/user/${mentionedUser.id}`,
|
|
documentId: document.id,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.type).toEqual(UnfurlResourceType.Mention);
|
|
expect(body.name).toEqual(mentionedUser.name);
|
|
});
|
|
|
|
it("should return 204 when internal document url points to non-existent document", async () => {
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `${env.URL}/doc/non-existent-doc-abc123`,
|
|
},
|
|
});
|
|
expect(res.status).toEqual(204);
|
|
});
|
|
|
|
it("should succeed with status 200 ok when valid document url is supplied", async () => {
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `${env.URL}/${document.url}`,
|
|
documentId: document.id,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.type).toEqual(UnfurlResourceType.Document);
|
|
expect(body.title).toEqual(document.titleWithDefault);
|
|
expect(body.id).toEqual(document.id);
|
|
});
|
|
|
|
it("should succeed with status 200 ok when valid share url is supplied", async () => {
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
const share = await buildShare({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
documentId: document.id,
|
|
published: true,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `${env.URL}/s/${share.id}/doc/${document.urlId}`,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.type).toEqual(UnfurlResourceType.Document);
|
|
expect(body.title).toEqual(document.titleWithDefault);
|
|
expect(body.id).toEqual(document.id);
|
|
});
|
|
|
|
it("should succeed with status 200 ok when share url with urlId is supplied", async () => {
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
const share = await buildShare({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
documentId: document.id,
|
|
urlId: "test-share",
|
|
published: true,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `${env.URL}/s/${share.urlId}/doc/${document.urlId}`,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.type).toEqual(UnfurlResourceType.Document);
|
|
expect(body.title).toEqual(document.titleWithDefault);
|
|
expect(body.id).toEqual(document.id);
|
|
});
|
|
|
|
it("should succeed with status 200 ok for share url without authentication", async () => {
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
const share = await buildShare({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
documentId: document.id,
|
|
published: true,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
url: `${env.URL}/s/${share.id}/doc/${document.urlId}`,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.type).toEqual(UnfurlResourceType.Document);
|
|
expect(body.title).toEqual(document.titleWithDefault);
|
|
expect(body.id).toEqual(document.id);
|
|
});
|
|
|
|
it("should return share-scoped url in unfurl response", async () => {
|
|
const document = await buildDocument({
|
|
teamId: user.teamId,
|
|
});
|
|
const share = await buildShare({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
documentId: document.id,
|
|
published: true,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `${env.URL}/s/${share.id}/doc/${document.urlId}`,
|
|
},
|
|
});
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
expect(body.url).toContain(`/s/${share.id}/doc/`);
|
|
});
|
|
|
|
it("should return 204 for collection share url without document", async () => {
|
|
const collection = await buildCollection({
|
|
teamId: user.teamId,
|
|
});
|
|
const share = await buildShare({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
published: true,
|
|
});
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: `${env.URL}/s/${share.id}`,
|
|
},
|
|
});
|
|
expect(res.status).toEqual(204);
|
|
});
|
|
|
|
it("should succeed with status 200 ok for a valid external url", async () => {
|
|
(Iframely.requestResource as Mock).mockResolvedValue(
|
|
Promise.resolve({
|
|
url: "https://www.flickr.com",
|
|
type: "rich",
|
|
meta: {
|
|
title: "Flickr",
|
|
description:
|
|
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
|
|
},
|
|
links: {
|
|
thumbnail: [
|
|
{
|
|
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg",
|
|
type: "image/jpg",
|
|
rel: ["twitter", "thumbnail", "ssl", "og"],
|
|
content_length: 412824,
|
|
media: {
|
|
width: 1200,
|
|
height: 630,
|
|
},
|
|
},
|
|
],
|
|
icon: [
|
|
{
|
|
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/67167dd041b0982f0f230dab_flickr-webclip.png",
|
|
rel: ["apple-touch-icon", "icon", "ssl"],
|
|
type: "image/png",
|
|
},
|
|
],
|
|
},
|
|
})
|
|
);
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "https://www.flickr.com",
|
|
},
|
|
});
|
|
|
|
expect(res.status).toEqual(200);
|
|
const body = await res.json();
|
|
|
|
expect(res.status).toEqual(200);
|
|
expect(body.url).toEqual("https://www.flickr.com");
|
|
expect(body.type).toEqual(UnfurlResourceType.URL);
|
|
expect(body.title).toEqual("Flickr");
|
|
expect(body.description).toEqual(
|
|
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!"
|
|
);
|
|
expect(body.thumbnailUrl).toEqual(
|
|
"https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg"
|
|
);
|
|
});
|
|
|
|
it("should succeed with status 204 no content for a non-existing external url", async () => {
|
|
(Iframely.requestResource as Mock).mockResolvedValue(
|
|
Promise.resolve({
|
|
status: 404,
|
|
error:
|
|
"Iframely could not fetch the given URL. The content is no longer available at the origin.",
|
|
})
|
|
);
|
|
|
|
const res = await server.post("/api/urls.unfurl", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "https://random.url",
|
|
},
|
|
});
|
|
|
|
expect(res.status).toEqual(204);
|
|
});
|
|
});
|
|
|
|
describe("#urls.checkEmbed", () => {
|
|
let user: User;
|
|
beforeEach(async () => {
|
|
user = await buildUser();
|
|
});
|
|
|
|
it("should fail with status 400 bad request when url is missing", async () => {
|
|
const res = await server.post("/api/urls.checkEmbed", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
},
|
|
});
|
|
|
|
expect(res.status).toEqual(400);
|
|
});
|
|
|
|
it("should fail with status 400 bad request when url is not a valid URL", async () => {
|
|
const res = await server.post("/api/urls.checkEmbed", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "not-a-url",
|
|
},
|
|
});
|
|
|
|
expect(res.status).toEqual(400);
|
|
});
|
|
|
|
it("should return a result for valid URLs", async () => {
|
|
// Use a YouTube URL which matches a known embed pattern
|
|
const res = await server.post("/api/urls.checkEmbed", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
expect(res.status).toEqual(200);
|
|
// Result depends on actual HTTP response from YouTube (or network error)
|
|
expect(body).toHaveProperty("embeddable");
|
|
});
|
|
});
|
|
|
|
describe("#urls.validateCustomDomain", () => {
|
|
it("should succeed with custom domain pointing at server", async () => {
|
|
const user = await buildUser();
|
|
const res = await server.post("/api/urls.validateCustomDomain", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
hostname: "valid.custom.domain",
|
|
},
|
|
});
|
|
expect(res.status).toEqual(200);
|
|
});
|
|
|
|
it("should fail with another domain", async () => {
|
|
const user = await buildUser();
|
|
const res = await server.post("/api/urls.validateCustomDomain", {
|
|
body: {
|
|
token: user.getJwtToken(),
|
|
hostname: "google.com",
|
|
},
|
|
});
|
|
expect(res.status).toEqual(400);
|
|
});
|
|
});
|