mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-07-01 08:16:26 +00:00
381 lines
10 KiB
TypeScript
381 lines
10 KiB
TypeScript
import {
|
|
afterAll,
|
|
afterEach,
|
|
beforeAll,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from "vitest";
|
|
|
|
let prisma: typeof import("@linkwarden/prisma").prisma;
|
|
let importFromHTMLFile: typeof import("./importFromHTMLFile").default;
|
|
let removeFolder: typeof import("@linkwarden/filesystem").removeFolder;
|
|
|
|
const createdUserIds: number[] = [];
|
|
|
|
const ensureTestEnv = async () => {
|
|
await import("dotenv/config");
|
|
|
|
if (!process.env.DATABASE_URL) {
|
|
throw new Error(
|
|
"DATABASE_URL must be set to run integration tests for importFromHTMLFile."
|
|
);
|
|
}
|
|
|
|
vi.stubEnv("NODE_ENV", "test");
|
|
process.env.STRIPE_SECRET_KEY = "";
|
|
process.env.NEXT_PUBLIC_STRIPE = "false";
|
|
process.env.NEXT_PUBLIC_REQUIRE_CC = "false";
|
|
process.env.MAX_LINKS_PER_USER = process.env.MAX_LINKS_PER_USER || "5";
|
|
process.env.STORAGE_FOLDER = process.env.STORAGE_FOLDER || "data-test";
|
|
|
|
delete process.env.SPACES_ENDPOINT;
|
|
delete process.env.SPACES_REGION;
|
|
delete process.env.SPACES_KEY;
|
|
delete process.env.SPACES_SECRET;
|
|
};
|
|
|
|
const createTestUser = async () => {
|
|
const suffix = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username: `import_test_${suffix}`,
|
|
email: `import_test_${suffix}@example.com`,
|
|
},
|
|
});
|
|
|
|
createdUserIds.push(user.id);
|
|
return user;
|
|
};
|
|
|
|
const cleanupUser = async (userId: number) => {
|
|
const collections = await prisma.collection.findMany({
|
|
where: { ownerId: userId },
|
|
select: { id: true },
|
|
});
|
|
|
|
try {
|
|
await prisma.user.delete({ where: { id: userId } });
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
|
|
for (const { id } of collections) {
|
|
await removeFolder({ filePath: `archives/${id}` });
|
|
}
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
await ensureTestEnv();
|
|
|
|
const prismaModule = await import("@linkwarden/prisma");
|
|
prisma = prismaModule.prisma;
|
|
|
|
const filesystemModule = await import("@linkwarden/filesystem");
|
|
removeFolder = filesystemModule.removeFolder;
|
|
|
|
importFromHTMLFile = (await import("./importFromHTMLFile")).default;
|
|
|
|
await prisma.$connect();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const users = createdUserIds.splice(0, createdUserIds.length);
|
|
for (const userId of users) {
|
|
await cleanupUser(userId);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
describe.sequential("importFromHTMLFile integration", () => {
|
|
it("returns an error when the link limit is exceeded", async () => {
|
|
const user = await createTestUser();
|
|
|
|
const collection = await prisma.collection.create({
|
|
data: {
|
|
name: "Existing",
|
|
owner: { connect: { id: user.id } },
|
|
createdBy: { connect: { id: user.id } },
|
|
},
|
|
});
|
|
|
|
for (let i = 0; i < 5; i += 1) {
|
|
await prisma.link.create({
|
|
data: {
|
|
name: `Existing ${i}`,
|
|
url: `https://example.com/existing-${i}`,
|
|
collectionId: collection.id,
|
|
createdById: user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
<html>
|
|
<body>
|
|
<DL><p>
|
|
<DT><A HREF="https://example.com/new">New</A></DT>
|
|
</DL><p>
|
|
</body>
|
|
</html>`;
|
|
|
|
const beforeCount = await prisma.link.count({
|
|
where: { createdById: user.id },
|
|
});
|
|
|
|
const result = await importFromHTMLFile(user.id, html);
|
|
|
|
const afterCount = await prisma.link.count({
|
|
where: { createdById: user.id },
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
response:
|
|
"Your subscription has reached the maximum number of links allowed.",
|
|
status: 400,
|
|
});
|
|
expect(afterCount).toBe(beforeCount);
|
|
});
|
|
|
|
it("imports root links into the Imports collection with tags, date, and description", async () => {
|
|
const user = await createTestUser();
|
|
|
|
const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
</head>
|
|
<body>
|
|
<P>Bookmarks</P>
|
|
<DL><p>
|
|
<DT><A HREF="https://example.com/path?q=fish&chips" ADD_DATE="1700000000" tags="news,tech">Example</A>
|
|
<DD>Example description</DD>
|
|
</DL><p>
|
|
</body>
|
|
</html>`;
|
|
|
|
const result = await importFromHTMLFile(user.id, html);
|
|
expect(result).toEqual({ response: "Success.", status: 200 });
|
|
|
|
const importsCollection = await prisma.collection.findFirst({
|
|
where: { ownerId: user.id, name: "Imports" },
|
|
});
|
|
|
|
expect(importsCollection).toBeTruthy();
|
|
|
|
const link = await prisma.link.findFirst({
|
|
where: {
|
|
collectionId: importsCollection?.id,
|
|
url: "https://example.com/path?q=fish&chips",
|
|
},
|
|
include: { tags: true },
|
|
});
|
|
|
|
expect(link).toBeTruthy();
|
|
expect(link?.description).toBe("Example description");
|
|
expect(link?.importDate?.toISOString()).toBe(
|
|
new Date(1700000000 * 1000).toISOString()
|
|
);
|
|
expect(link?.tags.map((tag) => tag.name).sort()).toEqual(["news", "tech"]);
|
|
});
|
|
|
|
it("creates nested collections and assigns links to the correct parent", async () => {
|
|
const user = await createTestUser();
|
|
|
|
const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
<html>
|
|
<body>
|
|
<DL><p>
|
|
<DT><H3>Recipes</H3>
|
|
<DL><p>
|
|
<DT><A HREF="https://example.com/soup">Soup</A></DT>
|
|
<DT><H3>Desserts</H3>
|
|
<DL><p>
|
|
<DT><A HREF="https://example.com/cake">Cake</A></DT>
|
|
</DL><p>
|
|
</DL><p>
|
|
</DL><p>
|
|
</body>
|
|
</html>`;
|
|
|
|
await importFromHTMLFile(user.id, html);
|
|
|
|
const recipesCollection = await prisma.collection.findFirst({
|
|
where: { ownerId: user.id, name: "Recipes" },
|
|
});
|
|
|
|
expect(recipesCollection).toBeTruthy();
|
|
|
|
const dessertsCollection = await prisma.collection.findFirst({
|
|
where: {
|
|
ownerId: user.id,
|
|
name: "Desserts",
|
|
parentId: recipesCollection?.id,
|
|
},
|
|
});
|
|
|
|
expect(dessertsCollection).toBeTruthy();
|
|
|
|
const soupLink = await prisma.link.findFirst({
|
|
where: { url: "https://example.com/soup" },
|
|
});
|
|
|
|
const cakeLink = await prisma.link.findFirst({
|
|
where: { url: "https://example.com/cake" },
|
|
});
|
|
|
|
expect(soupLink?.collectionId).toBe(recipesCollection?.id);
|
|
expect(cakeLink?.collectionId).toBe(dessertsCollection?.id);
|
|
});
|
|
|
|
it("reuses an existing Imports collection instead of creating a duplicate", async () => {
|
|
const user = await createTestUser();
|
|
|
|
const importsCollection = await prisma.collection.create({
|
|
data: {
|
|
name: "Imports",
|
|
owner: { connect: { id: user.id } },
|
|
createdBy: { connect: { id: user.id } },
|
|
},
|
|
});
|
|
|
|
const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
<html>
|
|
<body>
|
|
<DL><p>
|
|
<DT><A HREF="https://example.com/alpha">Alpha</A></DT>
|
|
<DT><A HREF="https://example.com/beta">Beta</A></DT>
|
|
</DL><p>
|
|
</body>
|
|
</html>`;
|
|
|
|
await importFromHTMLFile(user.id, html);
|
|
|
|
const importsCollections = await prisma.collection.findMany({
|
|
where: { ownerId: user.id, name: "Imports" },
|
|
});
|
|
|
|
expect(importsCollections).toHaveLength(1);
|
|
|
|
const importedLinks = await prisma.link.findMany({
|
|
where: { createdById: user.id },
|
|
});
|
|
|
|
expect(importedLinks).toHaveLength(2);
|
|
importedLinks.forEach((link) => {
|
|
expect(link.collectionId).toBe(importsCollection.id);
|
|
});
|
|
});
|
|
|
|
it("falls back to an Untitled Collection when a folder name is empty", async () => {
|
|
const user = await createTestUser();
|
|
|
|
const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
<html>
|
|
<body>
|
|
<DL><p>
|
|
<DT><H3></H3>
|
|
<DL><p>
|
|
<DT><A HREF="https://example.com/blank">Blank Folder</A></DT>
|
|
</DL><p>
|
|
</DL><p>
|
|
</body>
|
|
</html>`;
|
|
|
|
await importFromHTMLFile(user.id, html);
|
|
|
|
const untitledCollection = await prisma.collection.findFirst({
|
|
where: { ownerId: user.id, name: "Untitled Collection" },
|
|
});
|
|
|
|
expect(untitledCollection).toBeTruthy();
|
|
|
|
const link = await prisma.link.findFirst({
|
|
where: { url: "https://example.com/blank" },
|
|
});
|
|
|
|
expect(link?.collectionId).toBe(untitledCollection?.id);
|
|
});
|
|
|
|
it("skips invalid URLs and only creates links for valid URLs", async () => {
|
|
const user = await createTestUser();
|
|
|
|
const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
<html>
|
|
<body>
|
|
<DL><p>
|
|
<DT><A HREF="not a url">Broken</A></DT>
|
|
<DT><A HREF="https://valid.example.com">Valid</A></DT>
|
|
</DL><p>
|
|
</body>
|
|
</html>`;
|
|
|
|
await importFromHTMLFile(user.id, html);
|
|
|
|
const links = await prisma.link.findMany({
|
|
where: { createdById: user.id },
|
|
});
|
|
|
|
expect(links).toHaveLength(1);
|
|
expect(links[0]?.url).toBe("https://valid.example.com");
|
|
});
|
|
|
|
// it("keeps link ids in the same chronological order as importDate (createdAt fallback)", async () => {
|
|
// const user = await createTestUser();
|
|
// const nowSeconds = Math.floor(Date.now() / 1000);
|
|
// const olderSeconds = nowSeconds - 86400;
|
|
// const newerSeconds = nowSeconds + 86400;
|
|
|
|
// const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
// <html>
|
|
// <body>
|
|
// <DL><p>
|
|
// <DT><A HREF="https://example.com/old" ADD_DATE="${olderSeconds}">Old</A></DT>
|
|
// <DT><A HREF="https://example.com/new" ADD_DATE="${newerSeconds}">New</A></DT>
|
|
// <DT><A HREF="https://example.com/now">Now</A></DT>
|
|
|
|
// <DT><H3>Folder One</H3>
|
|
// <DL><p>
|
|
// <DT><A HREF="https://example.com/f1-old" ADD_DATE="${olderSeconds}">F1 Old</A></DT>
|
|
// <DT><A HREF="https://example.com/f1-new" ADD_DATE="${newerSeconds}">F1 New</A></DT>
|
|
// </DL><p>
|
|
|
|
// <DT><H3>Folder Two</H3>
|
|
// <DL><p>
|
|
// <DT><A HREF="https://example.com/f2-now">F2 Now</A></DT>
|
|
// <DT><A HREF="https://example.com/f2-newer" ADD_DATE="${newerSeconds}">F2 Newer</A></DT>
|
|
// </DL><p>
|
|
|
|
// </DL><p>
|
|
// </body>
|
|
// </html>`;
|
|
|
|
// await importFromHTMLFile(user.id, html);
|
|
|
|
// const linksById = await prisma.link.findMany({
|
|
// where: { createdById: user.id },
|
|
// orderBy: { id: "asc" },
|
|
// select: { id: true, importDate: true, createdAt: true, url: true },
|
|
// });
|
|
|
|
// console.log(linksById);
|
|
|
|
// expect(linksById).toHaveLength(3);
|
|
|
|
// const idsByIdOrder = linksById.map((link) => link.id);
|
|
// const idsByEffectiveDateOrder = [...linksById]
|
|
// .sort(
|
|
// (a, b) =>
|
|
// (a.importDate ?? a.createdAt).getTime() -
|
|
// (b.importDate ?? b.createdAt).getTime()
|
|
// )
|
|
// .map((link) => link.id);
|
|
|
|
// expect(idsByIdOrder).toEqual(idsByEffectiveDateOrder);
|
|
// });
|
|
});
|