-
Notifications
You must be signed in to change notification settings - Fork 0
Add unit tests for Inspiration Controller #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import { describe, it, expect, mock, beforeEach } from "bun:test"; | ||
|
|
||
| // Mock dependencies | ||
| const mockReadFile = mock(); | ||
| const mockCheckFileExists = mock(); | ||
| const mockLaunchBrowser = mock(); | ||
| const mockScrapeDribbble = mock(); | ||
| const mockCloseBrowser = mock(); | ||
|
|
||
| mock.module("../utils/fileSystem", () => ({ | ||
| readFile: mockReadFile, | ||
| checkFileExists: mockCheckFileExists | ||
| })); | ||
|
|
||
| mock.module("../services/scraperService", () => ({ | ||
| launchBrowser: mockLaunchBrowser, | ||
| scrapeDribbble: mockScrapeDribbble | ||
| })); | ||
|
|
||
| // Import the controller (CommonJS) | ||
| const { getInspiration, _resetCache } = require("../controllers/inspirationController"); | ||
|
|
||
| // Helper to mock response | ||
| const mockRes = () => { | ||
| const res = {}; | ||
| res.json = mock(() => res); | ||
| res.status = mock(() => res); | ||
| return res; | ||
| }; | ||
|
|
||
| // Test Data | ||
| const MOCK_CACHE_DATA = [ | ||
| { title: "Web Design 1", link: "link1", tags: ["web", "design"], source: "Dribbble" }, | ||
| { title: "App Design 1", link: "link2", tags: ["app", "ui"], source: "Behance" }, | ||
| { title: "Logo Design", link: "link3", tags: ["logo", "branding"], source: "Dribbble" }, | ||
| { title: "Web Dashboard", link: "link4", tags: ["web", "dashboard"], source: "Lapa" }, | ||
| { title: "Mobile App", link: "link5", tags: ["mobile", "app"], source: "Godly" }, | ||
| { title: "Landing Page", link: "link6", tags: ["landing", "page"], source: "SiteInspire" } | ||
| ]; | ||
|
|
||
| describe("Inspiration Controller", () => { | ||
| beforeEach(() => { | ||
| _resetCache(); | ||
| mockReadFile.mockReset(); | ||
| mockLaunchBrowser.mockReset(); | ||
| mockScrapeDribbble.mockReset(); | ||
| mockCloseBrowser.mockReset(); | ||
|
|
||
| // Default mock implementation | ||
| mockLaunchBrowser.mockResolvedValue({ close: mockCloseBrowser }); | ||
| }); | ||
|
|
||
| it("should serve all items from cache if no query provided", async () => { | ||
| mockReadFile.mockResolvedValue(JSON.stringify(MOCK_CACHE_DATA)); | ||
|
|
||
| const req = { query: {} }; | ||
| const res = mockRes(); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| expect(mockReadFile).toHaveBeenCalled(); | ||
| expect(res.json).toHaveBeenCalled(); | ||
| const result = res.json.mock.calls[0][0]; | ||
| expect(result.ok).toBe(true); | ||
| expect(result.count).toBe(MOCK_CACHE_DATA.length); | ||
| // Check if items are shuffled (order might differ, but set of IDs should be same) | ||
| const resultIds = result.items.map(i => i.link).sort(); | ||
| const expectedIds = MOCK_CACHE_DATA.map(i => i.link).sort(); | ||
| expect(resultIds).toEqual(expectedIds); | ||
| }); | ||
|
|
||
| it("should filter items based on query", async () => { | ||
| mockReadFile.mockResolvedValue(JSON.stringify(MOCK_CACHE_DATA)); | ||
|
|
||
| const req = { query: { q: "web" } }; | ||
| const res = mockRes(); | ||
|
|
||
| // Mock fallback to empty so we only check cache results + empty fallback | ||
| mockScrapeDribbble.mockResolvedValue([]); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| const result = res.json.mock.calls[0][0]; | ||
| // "Web Design 1" and "Web Dashboard" match "web" | ||
| // Scraper returns empty, so we expect 2 items. | ||
| expect(result.count).toBe(2); | ||
| expect(result.items.some(i => i.title === "Web Design 1")).toBe(true); | ||
| expect(result.items.some(i => i.title === "Web Dashboard")).toBe(true); | ||
| }); | ||
|
|
||
| it("should trigger fallback scraper if cache results are low (< 5) and query is present", async () => { | ||
| // Return full cache, but filtering "logo" only yields 1 result | ||
| mockReadFile.mockResolvedValue(JSON.stringify(MOCK_CACHE_DATA)); | ||
|
|
||
| const req = { query: { q: "logo" } }; | ||
| const res = mockRes(); | ||
|
|
||
| // Mock scraper results | ||
| const scraperResults = [ | ||
| { title: "New Logo 1", link: "new_link1", source: "Dribbble" }, | ||
| { title: "New Logo 2", link: "new_link2", source: "Dribbble" } | ||
| ]; | ||
| mockScrapeDribbble.mockResolvedValue(scraperResults); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| expect(mockLaunchBrowser).toHaveBeenCalled(); | ||
| expect(mockScrapeDribbble).toHaveBeenCalledWith(expect.anything(), "logo"); | ||
| expect(mockCloseBrowser).toHaveBeenCalled(); | ||
|
|
||
| const result = res.json.mock.calls[0][0]; | ||
| // Cache has "Logo Design" (1 match) | ||
| // Scraper returned 2 matches | ||
| // Total should be 3 | ||
| expect(result.items.length).toBe(3); | ||
| expect(result.items.some(i => i.title === "New Logo 1")).toBe(true); | ||
| }); | ||
|
|
||
| it("should NOT trigger fallback if query is empty", async () => { | ||
| // Empty query returns all cached items (6 items). 6 >= 5. | ||
| mockReadFile.mockResolvedValue(JSON.stringify(MOCK_CACHE_DATA)); | ||
|
|
||
| const req = { query: { q: "" } }; | ||
| const res = mockRes(); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| expect(mockLaunchBrowser).not.toHaveBeenCalled(); | ||
| const result = res.json.mock.calls[0][0]; | ||
| expect(result.count).toBe(6); | ||
| }); | ||
|
|
||
| it("should NOT trigger fallback if cache results are sufficient (>= 5) even with query", async () => { | ||
| // Return many matching items | ||
| const manyItems = [ | ||
| { title: "Web 1", link: "1", tags: ["web"] }, | ||
| { title: "Web 2", link: "2", tags: ["web"] }, | ||
| { title: "Web 3", link: "3", tags: ["web"] }, | ||
| { title: "Web 4", link: "4", tags: ["web"] }, | ||
| { title: "Web 5", link: "5", tags: ["web"] }, | ||
| ]; | ||
| mockReadFile.mockResolvedValue(JSON.stringify(manyItems)); | ||
|
|
||
| const req = { query: { q: "web" } }; | ||
| const res = mockRes(); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| expect(mockLaunchBrowser).not.toHaveBeenCalled(); | ||
| const result = res.json.mock.calls[0][0]; | ||
| expect(result.count).toBe(5); | ||
| }); | ||
|
|
||
| it("should handle cache read error gracefully", async () => { | ||
| mockReadFile.mockRejectedValue({ code: 'ENOENT' }); | ||
|
|
||
| const req = { query: { q: "test" } }; | ||
| const res = mockRes(); | ||
|
|
||
| // Scraper should trigger because cache is empty (0 < 5) and query is present | ||
| mockScrapeDribbble.mockResolvedValue([]); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| expect(mockReadFile).toHaveBeenCalled(); | ||
| // Should log warning but not crash | ||
| const result = res.json.mock.calls[0][0]; | ||
| expect(result.ok).toBe(true); | ||
| expect(result.items).toEqual([]); | ||
| }); | ||
|
|
||
| it("should handle scraper error gracefully", async () => { | ||
| mockReadFile.mockResolvedValue(JSON.stringify(MOCK_CACHE_DATA)); | ||
|
|
||
| const req = { query: { q: "logo" } }; // 1 result, triggers fallback | ||
| const res = mockRes(); | ||
|
|
||
| mockLaunchBrowser.mockRejectedValue(new Error("Browser launch failed")); | ||
|
|
||
| await getInspiration(req, res); | ||
|
|
||
| expect(mockLaunchBrowser).toHaveBeenCalled(); | ||
| // Controller catches error and continues with cached items | ||
| const result = res.json.mock.calls[0][0]; | ||
| expect(result.ok).toBe(true); | ||
| // Should contain filtered cache items (1 item) | ||
| expect(result.items.length).toBe(1); | ||
| expect(result.items[0].title).toBe("Logo Design"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||||||||||
| const fs = require('fs'); | ||||||||||||
|
|
||||||||||||
| async function readFile(filePath) { | ||||||||||||
| return fs.promises.readFile(filePath, 'utf-8'); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| function checkFileExists(filePath) { | ||||||||||||
| return fs.existsSync(filePath); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| module.exports = { | ||||||||||||
| readFile, | ||||||||||||
| checkFileExists | ||||||||||||
| }; | ||||||||||||
|
Comment on lines
+12
to
+14
|
||||||||||||
| readFile, | |
| checkFileExists | |
| }; | |
| readFile | |
| }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
mockCheckFileExistsfunction is mocked but never used in the code. ThecheckFileExistsexport fromfileSystem.jsis not used anywhere in the controller. This creates unnecessary test overhead and confusion. Consider removing this mock and the corresponding export fromfileSystem.jsif it's not needed.