hn-api.test.ts•7.36 kB
/**
* Integration Tests for HackerNews API Client
*
* These tests make real API calls to the HackerNews Algolia API.
* They verify that the client correctly handles API responses and errors.
*/
import { describe, expect, it } from "vitest";
import { HNAPIClient } from "../../src/services/hn-api.js";
// Use a longer timeout for integration tests with real API
const TEST_TIMEOUT = 10000;
describe(
"HNAPIClient - Real API Integration",
() => {
const client = new HNAPIClient();
describe("search", () => {
it("should return search results for a valid query", async () => {
const result = await client.search({
query: "JavaScript",
page: 0,
hitsPerPage: 10,
});
expect(result.hits).toBeInstanceOf(Array);
expect(result.nbHits).toBeGreaterThan(0);
expect(result.page).toBe(0);
expect(result.hitsPerPage).toBe(10);
expect(result.query).toBe("JavaScript");
});
it("should filter by tags", async () => {
const result = await client.search({
query: "Python",
tags: ["story"],
hitsPerPage: 5,
});
expect(result.hits).toBeInstanceOf(Array);
// All results should have 'story' tag
for (const hit of result.hits) {
expect(hit._tags).toContain("story");
}
});
it("should apply numeric filters", async () => {
const result = await client.search({
query: "AI",
numericFilters: ["points>=100"],
hitsPerPage: 10,
});
expect(result.hits).toBeInstanceOf(Array);
// All results should have at least 100 points
for (const hit of result.hits) {
if (hit.points !== null) {
expect(hit.points).toBeGreaterThanOrEqual(100);
}
}
});
it("should handle pagination", async () => {
const page0 = await client.search({
query: "JavaScript",
page: 0,
hitsPerPage: 5,
});
const page1 = await client.search({
query: "JavaScript",
page: 1,
hitsPerPage: 5,
});
expect(page0.hits).toHaveLength(5);
expect(page1.hits).toHaveLength(5);
// Results should be different
expect(page0.hits[0]?.objectID).not.toBe(page1.hits[0]?.objectID);
});
it("should handle empty query results", async () => {
const result = await client.search({
query: "veryrandomstringthatwontmatch12345xyz",
hitsPerPage: 10,
});
expect(result.hits).toBeInstanceOf(Array);
// May have zero hits
expect(result.nbHits).toBeGreaterThanOrEqual(0);
});
});
describe("searchByDate", () => {
it("should return results sorted by date", async () => {
const result = await client.searchByDate({
query: "",
tags: ["story"],
hitsPerPage: 10,
});
expect(result.hits).toBeInstanceOf(Array);
expect(result.hits.length).toBeGreaterThan(0);
// Check that results are sorted by date (newest first)
for (let i = 1; i < result.hits.length; i++) {
const prevDate = result.hits[i - 1]?.created_at_i ?? 0;
const currDate = result.hits[i]?.created_at_i ?? 0;
expect(prevDate).toBeGreaterThanOrEqual(currDate);
}
});
it("should work with empty query", async () => {
const result = await client.searchByDate({
query: "",
hitsPerPage: 5,
});
expect(result.hits).toBeInstanceOf(Array);
expect(result.hits.length).toBeGreaterThan(0);
});
it("should filter by tags", async () => {
const result = await client.searchByDate({
query: "",
tags: ["comment"],
hitsPerPage: 5,
});
expect(result.hits).toBeInstanceOf(Array);
for (const hit of result.hits) {
expect(hit._tags).toContain("comment");
}
});
});
describe("getItem", () => {
it("should fetch a known story with its metadata", async () => {
// Using a well-known HN post (the first one)
const result = await client.getItem("1");
expect(result).toBeDefined();
expect(result.id).toBe("1");
expect(result.type).toBeDefined();
expect(result.author).toBeDefined();
});
it(
"should include nested children for items with comments",
async () => {
// Using a story that likely has comments (recent popular story)
// Note: This test may need adjustment based on what's available
const searchResult = await client.search({
query: "",
tags: ["story", "front_page"],
numericFilters: ["num_comments>10"],
hitsPerPage: 1,
});
if (searchResult.hits.length > 0) {
const itemId = searchResult.hits[0]?.objectID;
if (itemId) {
const result = await client.getItem(itemId);
expect(result.id).toBe(itemId);
expect(result.children).toBeInstanceOf(Array);
// Should have at least some comments
expect(result.children.length).toBeGreaterThan(0);
}
}
},
TEST_TIMEOUT
);
it("should throw error for non-existent item", async () => {
// Using a very high number that likely doesn't exist
await expect(client.getItem("999999999999")).rejects.toThrow();
});
});
describe("getUser", () => {
it("should fetch a known user profile", async () => {
// Paul Graham is a well-known HN user
const result = await client.getUser("pg");
expect(result.username).toBe("pg");
expect(result.karma).toBeGreaterThan(0);
// about field is optional
if (result.about) {
expect(typeof result.about).toBe("string");
}
});
it("should include optional about field", async () => {
const result = await client.getUser("pg");
// about can be null or string
if (result.about !== null) {
expect(typeof result.about).toBe("string");
}
});
it("should throw error for non-existent user", async () => {
await expect(client.getUser("thisuserdefinitelydoesnotexist12345xyz")).rejects.toThrow();
});
});
describe("Error Handling", () => {
it("should handle network timeout", async () => {
// Create client with very short timeout
const shortTimeoutClient = new HNAPIClient(undefined, 1);
await expect(
shortTimeoutClient.search({
query: "test",
})
).rejects.toThrow(/timeout/i);
});
it("should handle invalid API responses gracefully", async () => {
// Create client with invalid base URL
const invalidClient = new HNAPIClient("https://invalid.example.com");
await expect(
invalidClient.search({
query: "test",
})
).rejects.toThrow();
});
});
describe("Query String Building", () => {
it("should handle multiple tags", async () => {
const result = await client.search({
query: "",
tags: ["story", "show_hn"],
hitsPerPage: 5,
});
expect(result.hits).toBeInstanceOf(Array);
});
it("should handle multiple numeric filters", async () => {
const result = await client.search({
query: "AI",
numericFilters: ["points>=50", "num_comments>=10"],
hitsPerPage: 5,
});
expect(result.hits).toBeInstanceOf(Array);
});
it("should handle all parameters combined", async () => {
const result = await client.search({
query: "JavaScript",
tags: ["story"],
numericFilters: ["points>=100"],
page: 0,
hitsPerPage: 5,
});
expect(result.hits).toBeInstanceOf(Array);
expect(result.page).toBe(0);
expect(result.hitsPerPage).toBe(5);
});
});
},
{ timeout: TEST_TIMEOUT }
);