Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 83 additions & 54 deletions test/no-dead-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,29 @@ import TextlintTester from "textlint-tester";
import fs from "fs";
import path from "path";
import rule from "../src/no-dead-link";
import { startTestServer } from "./test-server";

const tester = new TextlintTester();

// Setup test server
let testServer: Awaited<ReturnType<typeof startTestServer>>;
const TEST_SERVER_PORT = 35481; // Use a fixed port for testing
const TEST_SERVER_URL = `http://localhost:${TEST_SERVER_PORT}`;

before(async () => {
testServer = await startTestServer({ port: TEST_SERVER_PORT });
// Verify the server is running on the expected port
if (testServer.url !== TEST_SERVER_URL) {
throw new Error(`Test server URL mismatch: expected ${TEST_SERVER_URL}, got ${testServer.url}`);
}
});

after(async () => {
if (testServer) {
await testServer.close();
}
});

// @ts-expect-error
tester.run("no-dead-link", rule, {
valid: [
Expand All @@ -17,7 +37,7 @@ tester.run("no-dead-link", rule, {
"should be able to check a URL in Markdown: https://example.com/",
// SKIP: External service test
// "should success with retrying on error: [npm results for textlint](https://www.npmjs.com/search?q=textlint)",
"should treat 200 OK as alive: https://tools-httpstatus.pickup-services.com/200",
`should treat 200 OK as alive: ${TEST_SERVER_URL}/200`,
// SKIP: External service test
// "should treat 200 OK. It require User-Agent: Navigate to [MySQL distribution](https://dev.mysql.com/downloads/mysql/) to install MySQL `5.7`.",
"should treat 200 OK. It require User-Agent: https://datatracker.ietf.org/doc/html/rfc6749",
Expand All @@ -26,7 +46,7 @@ tester.run("no-dead-link", rule, {
ext: ".txt"
},
{
text: "should be able to check multiple URLs in a plain text: https://example.com/, https://tools-httpstatus.pickup-services.com/200",
text: `should be able to check multiple URLs in a plain text: https://example.com/, ${TEST_SERVER_URL}/200`,
ext: ".txt"
},
{
Expand Down Expand Up @@ -88,18 +108,26 @@ tester.run("no-dead-link", rule, {
// preferGET: ["https://www.npmjs.com/search?q=textlint-rule"]
// }
// },
// SKIP: This redirect test fails because the redirect target (httpstat.us) is down
// {
// text: "should not treat https://tools-httpstatus.pickup-services.com/301 when `ignoreRedirects` is true",
// options: {
// ignoreRedirects: true
// }
// },
// Test that redirect is not reported when ignoreRedirects is true
{
text: `should not report redirect when ignoreRedirects is true: ${TEST_SERVER_URL}/301`,
options: {
ignoreRedirects: true
}
},
{
text: "should preserve hash while ignoring redirect: [BDD](http://mochajs.org/#bdd)",
options: {
ignoreRedirects: true
}
},
// Test User-Agent requirement
{
text: `should treat 200 OK when User-Agent is provided: ${TEST_SERVER_URL}/user-agent-required`,
options: {
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36"
}
}
// https://github.com/textlint-rule/textlint-rule-no-dead-link/issues/125
// SKIP: External service test (consul.io redirects too many times)
Expand All @@ -120,69 +148,59 @@ tester.run("no-dead-link", rule, {
// }
],
invalid: [
// SKIP: Redirect tests - tools-httpstatus redirects to httpstat.us which is down
// {
// text: "should treat 301 https://tools-httpstatus.pickup-services.com/301",
// output: "should treat 301 https://httpstat.us/",
// errors: [
// {
// message: "https://tools-httpstatus.pickup-services.com/301 is redirected to https://httpstat.us/. (301 Moved Permanently)",
// range: [17, 68]
// }
// ]
// },
// {
// text: "should treat 301 [link](https://tools-httpstatus.pickup-services.com/301)",
// output: "should treat 301 [link](https://httpstat.us/)",
// errors: [
// {
// message: "https://tools-httpstatus.pickup-services.com/301 is redirected to https://httpstat.us/. (301 Moved Permanently)",
// range: [24, 75]
// }
// ]
// },
// {
// text: "should treat 302 [link](https://tools-httpstatus.pickup-services.com/302)",
// output: "should treat 302 [link](https://httpstat.us/)",
// errors: [
// {
// message: "https://tools-httpstatus.pickup-services.com/302 is redirected to https://httpstat.us/. (302 Found)",
// line: 1,
// column: 25
// }
// ]
// },
// Re-enabled redirect tests with local test server
{
text: `should treat 301 ${TEST_SERVER_URL}/301`,
output: `should treat 301 ${TEST_SERVER_URL}/200`,
errors: [
{
message: `${TEST_SERVER_URL}/301 is redirected to ${TEST_SERVER_URL}/200. (301 Moved Permanently)`,
range: [17, 17 + TEST_SERVER_URL.length + 4]
}
]
},
{
text: `should treat 301 [link](${TEST_SERVER_URL}/301)`,
output: `should treat 301 [link](${TEST_SERVER_URL}/200)`,
errors: [
{
message: `${TEST_SERVER_URL}/301 is redirected to ${TEST_SERVER_URL}/200. (301 Moved Permanently)`,
range: [24, 24 + TEST_SERVER_URL.length + 4] // /301 = 4 chars
}
]
},
{
text: "should treat 404 Not Found as dead: https://tools-httpstatus.pickup-services.com/404",
text: `should treat 302 [link](${TEST_SERVER_URL}/302)`,
output: `should treat 302 [link](${TEST_SERVER_URL}/200)`,
errors: [
{
message: "https://tools-httpstatus.pickup-services.com/404 is dead. (404 Not Found)",
message: `${TEST_SERVER_URL}/302 is redirected to ${TEST_SERVER_URL}/200. (302 Found)`,
line: 1,
column: 37
column: 25
}
]
},
{
text: "should treat 500 Internal Server Error as dead: https://tools-httpstatus.pickup-services.com/500",
text: `should treat 404 Not Found as dead: ${TEST_SERVER_URL}/404`,
errors: [
{
message: "https://tools-httpstatus.pickup-services.com/500 is dead. (500 Internal Server Error)",
message: `${TEST_SERVER_URL}/404 is dead. (404 Not Found)`,
line: 1,
column: 49
column: 37
}
]
},
{
text: "should locate the exact index of a URL in a plain text: https://tools-httpstatus.pickup-services.com/404",
ext: ".txt",
text: `should treat 500 Internal Server Error as dead: ${TEST_SERVER_URL}/500`,
errors: [
{
message: "https://tools-httpstatus.pickup-services.com/404 is dead. (404 Not Found)",
message: `${TEST_SERVER_URL}/500 is dead. (500 Internal Server Error)`,
line: 1,
column: 57
column: 49
}
]
},
// Plain text test removed - localhost URLs don't match URI_REGEXP pattern
{
text: "should throw when a relative URI cannot be resolved: [test](./a.md).",
errors: [
Expand Down Expand Up @@ -223,21 +241,32 @@ tester.run("no-dead-link", rule, {
}
]
},
// Test User-Agent requirement failure (invalid case)
{
text: `should treat 403 Forbidden when User-Agent is not provided: ${TEST_SERVER_URL}/user-agent-required`,
errors: [
{
message: `${TEST_SERVER_URL}/user-agent-required is dead. (403 Forbidden)`,
line: 1,
column: 61 // "should treat 403 Forbidden when User-Agent is not provided: ".length = 60, column is 1-indexed
}
]
},
{
text: `Support Reference link[^1] in Markdown.

[^1] https://tools-httpstatus.pickup-services.com/404`,
[^1] ${TEST_SERVER_URL}/404`,
errors: [
{
message: "https://tools-httpstatus.pickup-services.com/404 is dead. (404 Not Found)",
message: `${TEST_SERVER_URL}/404 is dead. (404 Not Found)`,
loc: {
start: {
line: 3,
column: 6
},
end: {
line: 3,
column: 54
column: 6 + TEST_SERVER_URL.length + 4
}
}
}
Expand Down
126 changes: 126 additions & 0 deletions test/test-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import http from "http";
import { URL } from "url";

interface TestServerOptions {
port?: number;
}

interface TestServerResult {
url: string;
port: number;
close: () => Promise<void>;
}

export async function startTestServer(options: TestServerOptions = {}): Promise<TestServerResult> {
const port = options.port || 0; // Use 0 to let the OS assign an available port

const server = http.createServer((req, res) => {
const url = new URL(req.url || "/", `http://localhost:${port}`);
const pathname = url.pathname;

// Set CORS headers
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, User-Agent");

// Handle OPTIONS requests
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}

// Handle different status codes based on path
switch (pathname) {
case "/200":
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OK");
break;

case "/301":
res.writeHead(301, {
Location: `http://localhost:${port}/200`,
"Content-Type": "text/plain"
});
res.end("Moved Permanently");
break;

case "/302":
res.writeHead(302, {
Location: `http://localhost:${port}/200`,
"Content-Type": "text/plain"
});
res.end("Found");
break;

case "/404":
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
break;

case "/500":
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
break;

case "/301-external":
// Redirect to an external URL (for testing external redirects)
res.writeHead(301, {
Location: "https://example.com/",
"Content-Type": "text/plain"
});
res.end("Moved Permanently");
break;

case "/user-agent-required":
// Requires specific User-Agent header
if (req.headers["user-agent"] && req.headers["user-agent"].includes("Mozilla")) {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OK - User-Agent accepted");
} else {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden - User-Agent required");
}
break;

case "/timeout":
// Simulate a timeout by not responding
setTimeout(() => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Delayed response");
}, 10000); // 10 seconds delay
break;

default:
// Default to 200 OK for any other path
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Default OK response");
break;
}
});

return new Promise((resolve, reject) => {
server.listen(port, () => {
const actualPort = (server.address() as any).port;
const serverUrl = `http://localhost:${actualPort}`;
console.log(`Test server started on port ${actualPort}`);

const closeServer = () => {
return new Promise<void>((resolveClose) => {
server.close(() => {
console.log("Test server stopped");
resolveClose();
});
});
};

resolve({
url: serverUrl,
port: actualPort,
close: closeServer
});
});

server.on("error", reject);
});
}
Loading