diff --git a/package-lock.json b/package-lock.json index 65aa212..da4f394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "core-assignment-week-10", "version": "1.0.0", - "hasInstallScript": true, "license": "CC BY-NC-SA 4.0", "dependencies": { "@inquirer/prompts": "^7.0.0", diff --git a/package.json b/package.json index 322f4c2..2f9b586 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,14 @@ }, "devDependencies": { "vitest": "^4.0.18" - } + }, + "main": "vitest.config.js", + "repository": { + "type": "git", + "url": "git+https://github.com/Unlock7/c55-core-week-10.git" + }, + "bugs": { + "url": "https://github.com/Unlock7/c55-core-week-10/issues" + }, + "homepage": "https://github.com/Unlock7/c55-core-week-10#readme" } diff --git a/task-1/cocktail.js b/task-1/cocktail.js index 6fd5a62..d7d771e 100644 --- a/task-1/cocktail.js +++ b/task-1/cocktail.js @@ -1,14 +1,15 @@ // API documentation: https://www.thecocktaildb.com/api.php -import path from 'path'; +import path from "path"; +import fs from "fs/promises"; -const BASE_URL = 'https://www.thecocktaildb.com/api/json/v1/1'; +const BASE_URL = "https://www.thecocktaildb.com/api/json/v1/1"; // Add helper functions as needed here export async function main() { if (process.argv.length < 3) { - console.error('Please provide a cocktail name as a command line argument.'); + console.error("Please provide a cocktail name as a command line argument."); return; } @@ -22,7 +23,54 @@ export async function main() { // 1. Fetch data from the API at the given URL // 2. Generate markdown content to match the examples // 3. Write the generated content to a markdown file as given by outPath + + const res = await fetch(url); + const data = await res.json(); + + if (!res.ok) { + const error = new Error(data.error || res.statusText); + + error.status = res.status; + + throw error; + } + + if (!data.drinks) { + console.error("No cocktails found with that name."); + return; + } + + let markdown = "# Cocktail Recipes\n\n"; + + for (const drink of data.drinks) { + markdown += `## ${drink.strDrink}\n\n`; + markdown += `![${drink.strDrink}](${drink.strDrinkThumb}/medium)\n\n`; + markdown += `**Category**: ${drink.strCategory}\n`; + + const isAlcoholic = drink.strAlcoholic === "Alcoholic" ? "Yes" : "No"; + markdown += `**Alcoholic**: ${isAlcoholic}\n\n`; + + markdown += `### Ingredients\n\n`; + for (let i = 1; i <= 15; i++) { + const ingredient = drink[`strIngredient${i}`]; + const measure = drink[`strMeasure${i}`]; + if (ingredient) { + const amount = measure ? `${measure.trim()} ` : ""; + markdown += `- ${amount}${ingredient.trim()}\n`; + } + } + + markdown += `\n### Instructions\n\n`; + markdown += `${drink.strInstructions}\n\n`; + markdown += `Serve in: ${drink.strGlass}\n\n`; + } + + const outputDir = path.dirname(outPath); + await fs.mkdir(outputDir, { recursive: true }); + await fs.writeFile(outPath, markdown.trim() + "\n"); } catch (error) { + console.error("Error occured:", error.message); + // 4. Handle errors } } diff --git a/task-1/examples/margarita.md b/task-1/examples/margarita.md index 86483bc..2fc30a3 100644 --- a/task-1/examples/margarita.md +++ b/task-1/examples/margarita.md @@ -129,4 +129,3 @@ Serve in: Cocktail glass In a mason jar muddle the watermelon and 5 mint leaves together into a puree and strain. Next add the grapefruit juice, juice of half a lime and the tequila as well as some ice. Put a lid on the jar and shake. Pour into a glass and add more ice. Garnish with fresh mint and a small slice of watermelon. Serve in: Collins glass - diff --git a/task-1/output/margarita.md b/task-1/output/margarita.md new file mode 100644 index 0000000..4be0715 --- /dev/null +++ b/task-1/output/margarita.md @@ -0,0 +1,21 @@ +# Cocktail Recipes + +## Margarita + +![Margarita](https://www.thecocktaildb.com/images/media/drink/5noda61589575158.jpg/medium) + +**Category**: Ordinary Drink +**Alcoholic**: Yes + +### Ingredients + +- 1 1/2 oz Tequila +- 1/2 oz Triple sec +- 1 oz Lime juice +- Salt + +### Instructions + +Rub the rim of the glass. + +Serve in: Cocktail glass diff --git a/task-1/tests/cocktail.test.js b/task-1/tests/cocktail.test.js index 185945d..9bd51ec 100644 --- a/task-1/tests/cocktail.test.js +++ b/task-1/tests/cocktail.test.js @@ -1,22 +1,22 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { beforeAll, describe, expect, test, vi } from 'vitest'; +import fs from "fs/promises"; +import path from "path"; +import { beforeAll, describe, expect, test, vi } from "vitest"; -import { main } from '../cocktail.js'; +import { main } from "../cocktail.js"; -import { data } from './drinks.js'; +import { data } from "./drinks.js"; let lines = []; beforeAll(async () => { const originalArgv = process.argv; - process.argv = ['node', 'main.js', 'margarita']; // Example cocktail name + process.argv = ["node", "main.js", "margarita"]; // Example cocktail name global.fetch = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(data), - }) + }), ); // Mock console.log to suppress output during test @@ -34,69 +34,69 @@ beforeAll(async () => { console.log = originalConsoleLog; try { - const content = await fs.readFile(outPath, 'utf-8'); - lines = content.trim().split('\n'); + const content = await fs.readFile(outPath, "utf-8"); + lines = content.trim().split("\n"); } catch (error) { - console.error('Error reading generated markdown file:', error); + console.error("Error reading generated markdown file:", error); } }); -describe('Content tests', () => { - test('[10] Markdown file created', async () => { +describe("Content tests", () => { + test("[10] Markdown file created", async () => { expect(lines.length).toBeGreaterThan(0); }); - test('[1] Content starts with # Cocktail Recipes', async () => { - expect(lines[0]).toBe('# Cocktail Recipes'); + test("[1] Content starts with # Cocktail Recipes", async () => { + expect(lines[0]).toBe("# Cocktail Recipes"); }); - test('[1] Content includes drink name as ## Margarita', async () => { - expect(lines).toContain('## Margarita'); + test("[1] Content includes drink name as ## Margarita", async () => { + expect(lines).toContain("## Margarita"); }); - test('[1] Content includes medium drink image', async () => { + test("[1] Content includes medium drink image", async () => { expect(lines).toContain( - '![Margarita](https://www.thecocktaildb.com/images/media/drink/5noda61589575158.jpg/medium)' + "![Margarita](https://www.thecocktaildb.com/images/media/drink/5noda61589575158.jpg/medium)", ); }); - test('[1] Content includes category and alcoholic info', async () => { - expect(lines).toContain('**Category**: Ordinary Drink'); - expect(lines).toContain('**Alcoholic**: Yes'); + test("[1] Content includes category and alcoholic info", async () => { + expect(lines).toContain("**Category**: Ordinary Drink"); + expect(lines).toContain("**Alcoholic**: Yes"); }); - test('[1] Content includes header ### Ingredients', async () => { - expect(lines).toContain('### Ingredients'); + test("[1] Content includes header ### Ingredients", async () => { + expect(lines).toContain("### Ingredients"); }); - test('[1] Content includes ingredients list', async () => { - expect(lines).toContain('- 1 1/2 oz Tequila'); - expect(lines).toContain('- 1/2 oz Triple sec'); - expect(lines).toContain('- 1 oz Lime juice'); - expect(lines).toContain('- Salt'); + test("[1] Content includes ingredients list", async () => { + expect(lines).toContain("- 1 1/2 oz Tequila"); + expect(lines).toContain("- 1/2 oz Triple sec"); + expect(lines).toContain("- 1 oz Lime juice"); + expect(lines).toContain("- Salt"); }); - test('[1] Content includes header ### Instructions', async () => { - expect(lines).toContain('### Instructions'); + test("[1] Content includes header ### Instructions", async () => { + expect(lines).toContain("### Instructions"); }); - test('[1] Content includes instruction details', async () => { - expect(lines).toContain('Rub the rim of the glass.'); + test("[1] Content includes instruction details", async () => { + expect(lines).toContain("Rub the rim of the glass."); }); - test('[1] Content includes glass type', async () => { - expect(lines).toContain('Serve in: Cocktail glass'); + test("[1] Content includes glass type", async () => { + expect(lines).toContain("Serve in: Cocktail glass"); }); }); -describe('Error handling', () => { - test('[1] Calls console.error() if no cocktail name argument provided', async () => { +describe("Error handling", () => { + test("[1] Calls console.error() if no cocktail name argument provided", async () => { const originalArgv = process.argv; - process.argv = ['node', 'main.js']; // No cocktail name + process.argv = ["node", "main.js"]; // No cocktail name // Mock console.log to capture output const consoleErrorMock = vi - .spyOn(console, 'error') + .spyOn(console, "error") .mockImplementation(() => {}); await main(); @@ -108,26 +108,26 @@ describe('Error handling', () => { consoleErrorMock.mockRestore(); }); - test('[1] Calls console.error() if cocktail not found', async () => { + test("[1] Calls console.error() if cocktail not found", async () => { const originalArgv = process.argv; - process.argv = ['node', 'main.js', 'nonexistentcocktail']; // Example cocktail name + process.argv = ["node", "main.js", "nonexistentcocktail"]; // Example cocktail name global.fetch = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ drinks: null }), - }) + }), ); // Mock console.log to capture output const consoleLogMock = vi - .spyOn(console, 'error') + .spyOn(console, "error") .mockImplementation(() => {}); await main(); expect(consoleLogMock).toHaveBeenCalledWith( - expect.stringContaining('No cocktails found with that name.') + expect.stringContaining("No cocktails found with that name."), ); // Restore original process.argv and console.log @@ -137,21 +137,21 @@ describe('Error handling', () => { expect(fetch).toHaveBeenCalledTimes(1); }); - test('[1] Calls console.error() if fetch was not OK', async () => { + test("[1] Calls console.error() if fetch was not OK", async () => { const originalArgv = process.argv; - process.argv = ['node', 'main.js', 'margarita']; // Example cocktail name + process.argv = ["node", "main.js", "margarita"]; // Example cocktail name global.fetch = vi.fn(() => Promise.resolve({ ok: false, status: 500, json: () => Promise.resolve({}), - }) + }), ); // Mock console.log to suppress output during test const consoleErrorMock = vi - .spyOn(console, 'error') + .spyOn(console, "error") .mockImplementation(() => {}); await main(); diff --git a/task-2/post-cli/src/services.js b/task-2/post-cli/src/services.js index 2601f57..4a480c1 100644 --- a/task-2/post-cli/src/services.js +++ b/task-2/post-cli/src/services.js @@ -1,5 +1,5 @@ // Change base URL for API requests to the local IP of the Post Central API server -const BASE_URL = 'http://localhost:3000'; +const BASE_URL = "https://postcentral.hyf.dev"; // ============================================================================ // AUTH TOKEN - Stored after login/register, sent with every request @@ -37,7 +37,7 @@ const getHello = async () => { const response = await fetch(`${BASE_URL}/posts/hello`); if (!response.ok) { throw new Error( - `Failed to get hello: HTTP ${response.status} ${response.statusText}` + `Failed to get hello: HTTP ${response.status} ${response.statusText}`, ); } return await response.json(); @@ -52,7 +52,29 @@ const getHello = async () => { * Method: GET | Endpoint: /users/me | Auth: Yes * Response: { user: string } */ + const getMe = async () => { + try { + const response = await fetch(`${BASE_URL}/users/me`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${getToken()}`, + }, + }); + + if (!response.ok) { + const error = new Error( + `Failed to get usesname: HTTP ${response.status} ${response.statusText}`, + ); + error.status = response.status; + throw error; + } + return await response.json(); + } catch (err) { + console.error("Error in GetMe:", err.message); + throw err; + } // TODO }; @@ -67,16 +89,17 @@ const getMe = async () => { * Response: { user: string, token: string } */ const createUser = async (name, password) => { + // TODO const response = await fetch(`${BASE_URL}/users/register`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ name, password }), }); if (!response.ok) { throw new Error( - `Failed to create user: HTTP ${response.status} ${response.statusText}` + `Failed to create user: HTTP ${response.status} ${response.statusText}`, ); } return await response.json(); @@ -90,6 +113,19 @@ const createUser = async (name, password) => { */ const loginUser = async (name, password) => { // TODO + const response = await fetch(`${BASE_URL}/users/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, password }), + }); + if (!response.ok) { + throw new Error( + `Failed to login: HTTP ${response.status} ${response.statusText}`, + ); + } + return await response.json(); }; // ============================================================================ @@ -104,6 +140,20 @@ const loginUser = async (name, password) => { */ const createPost = async (text) => { // TODO + const response = await fetch(`${BASE_URL}/posts`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${getToken()}`, + }, + body: JSON.stringify({ text }), + }); + if (!response.ok) + throw new Error( + `Failed to create post: Http ${response.status} ${response.statusText}`, + ); + + return await response.json(); }; /** @@ -113,6 +163,18 @@ const createPost = async (text) => { */ const getPosts = async () => { // TODO + const response = await fetch(`${BASE_URL}/posts/me`, { + method: "GET", + headers: { + Authorization: `Bearer ${getToken()}`, + }, + }); + if (!response.ok) { + throw new Error( + `Failed to get post ${response.status} ${response.statusText}`, + ); + } + return await response.json(); }; /** @@ -123,6 +185,20 @@ const getPosts = async () => { */ const updatePost = async (id, text) => { // TODO + const response = await fetch(`${BASE_URL}/posts/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${getToken()}`, + }, + body: JSON.stringify({ text }), + }); + if (!response.ok) { + throw new Error( + `Failed to update posts ${response.status} ${response.statusText}`, + ); + } + return await response.json(); }; /** @@ -132,6 +208,18 @@ const updatePost = async (id, text) => { */ const deleteUser = async () => { // TODO + const response = await fetch(`${BASE_URL}/users/me`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${getToken()}`, + }, + }); + if (!response.ok) { + throw new Error( + `Failed to delete user: http ${response.status} ${response.statusText}`, + ); + } + return await response.json(); }; /** @@ -141,6 +229,18 @@ const deleteUser = async () => { */ const deletePost = async (id) => { // TODO + const response = await fetch(` ${BASE_URL}/posts/${id}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${getToken()}`, + }, + }); + if (!response.ok) { + throw new Error( + `Failed to delete posts: HTTP ${response.status} ${response.statusText}`, + ); + } + return await response.json(); }; // ============================================================================ diff --git a/task-2/post-cli/src/services.starter.js b/task-2/post-cli/src/services.starter.js index 2601f57..6620706 100644 --- a/task-2/post-cli/src/services.starter.js +++ b/task-2/post-cli/src/services.starter.js @@ -1,5 +1,5 @@ // Change base URL for API requests to the local IP of the Post Central API server -const BASE_URL = 'http://localhost:3000'; +const BASE_URL = "https://postcentral.hyf.dev"; // ============================================================================ // AUTH TOKEN - Stored after login/register, sent with every request @@ -37,7 +37,7 @@ const getHello = async () => { const response = await fetch(`${BASE_URL}/posts/hello`); if (!response.ok) { throw new Error( - `Failed to get hello: HTTP ${response.status} ${response.statusText}` + `Failed to get hello: HTTP ${response.status} ${response.statusText}`, ); } return await response.json(); @@ -68,15 +68,15 @@ const getMe = async () => { */ const createUser = async (name, password) => { const response = await fetch(`${BASE_URL}/users/register`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ name, password }), }); if (!response.ok) { throw new Error( - `Failed to create user: HTTP ${response.status} ${response.statusText}` + `Failed to create user: HTTP ${response.status} ${response.statusText}`, ); } return await response.json(); diff --git a/task-2/post-cli/tests/test-crud.test.js b/task-2/post-cli/tests/test-crud.test.js index fbf7ea4..bd111f9 100644 --- a/task-2/post-cli/tests/test-crud.test.js +++ b/task-2/post-cli/tests/test-crud.test.js @@ -6,14 +6,14 @@ * without requiring a running API server. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Import fetcher functions let services; try { - services = await import('../src/services.solution.js'); + services = await import("../src/services.solution.js"); } catch { - services = await import('../src/services.js'); + services = await import("../src/services.js"); } const { createPost, @@ -26,12 +26,12 @@ const { updatePost, } = services; -describe('Complete CRUD Operations', () => { +describe("Complete CRUD Operations", () => { let fetchMock; beforeEach(() => { // Mock the global fetch function before each test - fetchMock = vi.spyOn(global, 'fetch'); + fetchMock = vi.spyOn(global, "fetch"); }); afterEach(() => { @@ -40,18 +40,18 @@ describe('Complete CRUD Operations', () => { setToken(null); }); - it('should complete all CRUD operations successfully', async () => { - const testUser = 'Alice'; - const testPassword = 'password123'; - const postText = 'My first test post!'; - const updatedText = 'Updated post text!'; + it("should complete all CRUD operations successfully", async () => { + const testUser = "Alice"; + const testPassword = "password123"; + const postText = "My first test post!"; + const updatedText = "Updated post text!"; const postId = 1; // Mock CREATE user response fetchMock.mockResolvedValueOnce({ ok: true, status: 201, - json: async () => ({ user: testUser, token: 'jwt-token-abc' }), + json: async () => ({ user: testUser, token: "jwt-token-abc" }), }); // CREATE: Register a user @@ -59,11 +59,11 @@ describe('Complete CRUD Operations', () => { expect(data.user).toBe(testUser); expect(data.token).toBeDefined(); expect(fetchMock).toHaveBeenLastCalledWith( - expect.stringContaining('/users/register'), + expect.stringContaining("/users/register"), expect.objectContaining({ - method: 'POST', + method: "POST", body: JSON.stringify({ name: testUser, password: testPassword }), - }) + }), ); // Set token for subsequent authenticated requests @@ -81,14 +81,14 @@ describe('Complete CRUD Operations', () => { expect(data.id).toBe(postId); expect(data.text).toBe(postText); expect(fetchMock).toHaveBeenLastCalledWith( - expect.stringContaining('/posts'), + expect.stringContaining("/posts"), expect.objectContaining({ - method: 'POST', + method: "POST", headers: expect.objectContaining({ - Authorization: 'Bearer jwt-token-abc', + Authorization: "Bearer jwt-token-abc", }), body: JSON.stringify({ text: postText }), - }) + }), ); // Mock READ posts response @@ -104,12 +104,12 @@ describe('Complete CRUD Operations', () => { expect(Array.isArray(data)).toBe(true); expect(data.length).toBeGreaterThan(0); expect(fetchMock).toHaveBeenLastCalledWith( - expect.stringContaining('/posts/me'), + expect.stringContaining("/posts/me"), expect.objectContaining({ headers: expect.objectContaining({ - Authorization: 'Bearer jwt-token-abc', + Authorization: "Bearer jwt-token-abc", }), - }) + }), ); // Mock UPDATE post response @@ -126,12 +126,12 @@ describe('Complete CRUD Operations', () => { expect(fetchMock).toHaveBeenLastCalledWith( expect.stringContaining(`/posts/${postId}`), expect.objectContaining({ - method: 'PUT', + method: "PUT", headers: expect.objectContaining({ - Authorization: 'Bearer jwt-token-abc', + Authorization: "Bearer jwt-token-abc", }), body: JSON.stringify({ text: updatedText }), - }) + }), ); // Mock READ updated posts response @@ -152,6 +152,11 @@ describe('Complete CRUD Operations', () => { fetchMock.mockResolvedValueOnce({ ok: true, status: 200, + json: async () => ({ + id: 1, + text: "Updated post text!", + message: "Post deleted", + }), }); // DELETE: Remove the post @@ -159,36 +164,37 @@ describe('Complete CRUD Operations', () => { expect(fetchMock).toHaveBeenLastCalledWith( expect.stringContaining(`/posts/${postId}`), expect.objectContaining({ - method: 'DELETE', + method: "DELETE", headers: expect.objectContaining({ - Authorization: 'Bearer jwt-token-abc', + Authorization: "Bearer jwt-token-abc", }), - }) + }), ); // Mock DELETE user response fetchMock.mockResolvedValueOnce({ ok: true, status: 200, + json: async () => ({ user: "Alice", message: "User deleted" }), }); // DELETE: Remove the user await deleteUser(); expect(fetchMock).toHaveBeenLastCalledWith( - 'http://localhost:3000/users/me', + "https://postcentral.hyf.dev/users/me", expect.objectContaining({ - method: 'DELETE', + method: "DELETE", headers: expect.objectContaining({ - Authorization: 'Bearer jwt-token-abc', + Authorization: "Bearer jwt-token-abc", }), - }) + }), ); }); - it('should verify updatePost uses PUT method with Authorization', async () => { + it("should verify updatePost uses PUT method with Authorization", async () => { const postId = 42; - const newText = 'Updated content'; - setToken('mock-jwt-token'); + const newText = "Updated content"; + setToken("mock-jwt-token"); fetchMock.mockResolvedValueOnce({ ok: true, @@ -202,63 +208,65 @@ describe('Complete CRUD Operations', () => { expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining(`/posts/${postId}`), expect.objectContaining({ - method: 'PUT', + method: "PUT", headers: expect.objectContaining({ - 'Content-Type': 'application/json', - Authorization: 'Bearer mock-jwt-token', + "Content-Type": "application/json", + Authorization: "Bearer mock-jwt-token", }), body: JSON.stringify({ text: newText }), - }) + }), ); }); - it('should verify deleteUser uses DELETE method with Authorization', async () => { - setToken('mock-jwt-token'); + it("should verify deleteUser uses DELETE method with Authorization", async () => { + setToken("mock-jwt-token"); fetchMock.mockResolvedValueOnce({ ok: true, status: 200, + json: async () => ({ user: "mock-user", message: "User deleted" }), }); await deleteUser(); // Verify DELETE request to correct endpoint with auth header expect(fetchMock).toHaveBeenCalledWith( - expect.stringContaining('/users/me'), + expect.stringContaining("/users/me"), expect.objectContaining({ - method: 'DELETE', + method: "DELETE", headers: expect.objectContaining({ - Authorization: 'Bearer mock-jwt-token', + Authorization: "Bearer mock-jwt-token", }), - }) + }), ); }); - it('should verify deletePost uses DELETE method with Authorization', async () => { - setToken('mock-jwt-token'); + it("should verify deletePost uses DELETE method with Authorization", async () => { + setToken("mock-jwt-token"); fetchMock.mockResolvedValueOnce({ ok: true, status: 200, + json: async () => ({ id: 5, text: "some text", message: "Post deleted" }), }); await deletePost(5); // Verify DELETE request with correct URL and auth header expect(fetchMock).toHaveBeenCalledWith( - expect.stringContaining('/posts/5'), + expect.stringContaining("/posts/5"), expect.objectContaining({ - method: 'DELETE', + method: "DELETE", headers: expect.objectContaining({ - Authorization: 'Bearer mock-jwt-token', + Authorization: "Bearer mock-jwt-token", }), - }) + }), ); }); - it('should verify getMe fetches current user info with Authorization', async () => { - setToken('mock-jwt-token'); - const mockUser = { user: 'Alice' }; + it("should verify getMe fetches current user info with Authorization", async () => { + setToken("mock-jwt-token"); + const mockUser = { user: "Alice" }; fetchMock.mockResolvedValueOnce({ ok: true, @@ -270,26 +278,26 @@ describe('Complete CRUD Operations', () => { expect(data).toEqual(mockUser); expect(fetchMock).toHaveBeenCalledWith( - expect.stringContaining('/users/me'), + expect.stringContaining("/users/me"), expect.objectContaining({ headers: expect.objectContaining({ - Authorization: 'Bearer mock-jwt-token', + Authorization: "Bearer mock-jwt-token", }), - }) + }), ); expect(fetchMock).toHaveBeenCalledTimes(1); }); - it('should handle errors in any CRUD operation', async () => { + it("should handle errors in any CRUD operation", async () => { // Mock error response fetchMock.mockResolvedValueOnce({ ok: false, status: 500, - statusText: 'Internal Server Error', - json: async () => ({ error: 'Server error' }), + statusText: "Internal Server Error", + json: async () => ({ error: "Server error" }), }); // Verify any function throws error on API failure - await expect(createUser('Test', 'pass')).rejects.toThrow(); + await expect(createUser("Test", "pass")).rejects.toThrow(); }); }); diff --git a/task-2/post-cli/tests/test-post.test.js b/task-2/post-cli/tests/test-post.test.js index 0431c68..4a3c3a7 100644 --- a/task-2/post-cli/tests/test-post.test.js +++ b/task-2/post-cli/tests/test-post.test.js @@ -6,23 +6,23 @@ * without requiring a running API server. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Import fetcher functions let services; try { - services = await import('../src/services.solution.js'); + services = await import("../src/services.solution.js"); } catch { - services = await import('../src/services.js'); + services = await import("../src/services.js"); } const { createPost, createUser, loginUser, setToken } = services; -describe('POST Functions', () => { +describe("POST Functions", () => { let fetchMock; beforeEach(() => { // Mock the global fetch function before each test - fetchMock = vi.spyOn(global, 'fetch'); + fetchMock = vi.spyOn(global, "fetch"); }); afterEach(() => { @@ -31,36 +31,36 @@ describe('POST Functions', () => { setToken(null); }); - describe('createUser()', () => { - it('should make POST request to /users/register with correct data', async () => { - const testName = 'Alice'; - const testPassword = 'testpass123'; + describe("createUser()", () => { + it("should make POST request to /users/register with correct data", async () => { + const testName = "Alice"; + const testPassword = "testpass123"; fetchMock.mockResolvedValueOnce({ ok: true, status: 201, - json: async () => ({ user: testName, token: 'jwt-token-abc' }), + json: async () => ({ user: testName, token: "jwt-token-abc" }), }); await createUser(testName, testPassword); // Verify fetch was called with correct parameters expect(fetchMock).toHaveBeenCalledWith( - 'http://localhost:3000/users/register', + "https://postcentral.hyf.dev/users/register", { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ name: testName, password: testPassword }), - } + }, ); expect(fetchMock).toHaveBeenCalledTimes(1); }); - it('should return user data with correct structure', async () => { - const testName = 'Alice'; - const mockData = { user: testName, token: 'jwt-token-abc' }; + it("should return user data with correct structure", async () => { + const testName = "Alice"; + const mockData = { user: testName, token: "jwt-token-abc" }; fetchMock.mockResolvedValueOnce({ ok: true, @@ -68,126 +68,129 @@ describe('POST Functions', () => { json: async () => mockData, }); - const data = await createUser(testName, 'testpass123'); + const data = await createUser(testName, "testpass123"); // Verify the function returns the expected data structure expect(data).toBeDefined(); - expect(data).toHaveProperty('user'); - expect(data).toHaveProperty('token'); + expect(data).toHaveProperty("user"); + expect(data).toHaveProperty("token"); expect(data.user).toBe(testName); - expect(typeof data.token).toBe('string'); + expect(typeof data.token).toBe("string"); }); - it('should include Content-Type header', async () => { + it("should include Content-Type header", async () => { fetchMock.mockResolvedValueOnce({ ok: true, status: 201, - json: async () => ({ user: 'Alice', token: 'jwt-token-abc' }), + json: async () => ({ user: "Alice", token: "jwt-token-abc" }), }); - await createUser('Alice', 'testpass123'); + await createUser("Alice", "testpass123"); // Check that headers include Content-Type const callArgs = fetchMock.mock.calls[0][1]; - expect(callArgs.headers).toHaveProperty('Content-Type'); - expect(callArgs.headers['Content-Type']).toBe('application/json'); + expect(callArgs.headers).toHaveProperty("Content-Type"); + expect(callArgs.headers["Content-Type"]).toBe("application/json"); }); - it('should throw error when API returns error', async () => { + it("should throw error when API returns error", async () => { // Mock 400 response (bad request) fetchMock.mockResolvedValueOnce({ ok: false, status: 400, - statusText: 'Bad Request', - json: async () => ({ error: 'Name is required' }), + statusText: "Bad Request", + json: async () => ({ error: "Name is required" }), }); // Verify the function throws an error - await expect(createUser('', '')).rejects.toThrow(); + await expect(createUser("", "")).rejects.toThrow(); }); }); - describe('loginUser()', () => { - it('should make POST request to /users/login with correct data', async () => { + describe("loginUser()", () => { + it("should make POST request to /users/login with correct data", async () => { fetchMock.mockResolvedValueOnce({ ok: true, status: 200, - json: async () => ({ user: 'Alice', token: 'jwt-token-xyz' }), + json: async () => ({ user: "Alice", token: "jwt-token-xyz" }), }); - await loginUser('Alice', 'password123'); + await loginUser("Alice", "password123"); // Verify fetch was called with correct parameters expect(fetchMock).toHaveBeenCalledWith( - 'http://localhost:3000/users/login', + "https://postcentral.hyf.dev/users/login", { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify({ name: 'Alice', password: 'password123' }), - } + body: JSON.stringify({ name: "Alice", password: "password123" }), + }, ); }); - it('should return user data with token', async () => { + it("should return user data with token", async () => { fetchMock.mockResolvedValueOnce({ ok: true, status: 200, - json: async () => ({ user: 'Alice', token: 'jwt-token-xyz' }), + json: async () => ({ user: "Alice", token: "jwt-token-xyz" }), }); - const data = await loginUser('Alice', 'password123'); + const data = await loginUser("Alice", "password123"); - expect(data).toHaveProperty('user'); - expect(data).toHaveProperty('token'); - expect(data.user).toBe('Alice'); + expect(data).toHaveProperty("user"); + expect(data).toHaveProperty("token"); + expect(data.user).toBe("Alice"); }); - it('should throw error on invalid credentials', async () => { + it("should throw error on invalid credentials", async () => { fetchMock.mockResolvedValueOnce({ ok: false, status: 401, - statusText: 'Unauthorized', - json: async () => ({ error: 'Invalid credentials' }), + statusText: "Unauthorized", + json: async () => ({ error: "Invalid credentials" }), }); - await expect(loginUser('Alice', 'wrongpass')).rejects.toThrow(); + await expect(loginUser("Alice", "wrongpass")).rejects.toThrow(); }); }); - describe('createPost()', () => { + describe("createPost()", () => { beforeEach(() => { // Set token for authenticated requests - setToken('mock-jwt-token'); + setToken("mock-jwt-token"); }); - it('should make POST request to /posts with correct data and Authorization header', async () => { - const postText = 'My test post!'; + it("should make POST request to /posts with correct data and Authorization header", async () => { + const postText = "My test post!"; fetchMock.mockResolvedValueOnce({ ok: true, status: 201, - json: async () => ({ id: 1, text: postText, user: 'Alice' }), + json: async () => ({ id: 1, text: postText, user: "Alice" }), }); await createPost(postText); // Verify fetch was called with correct parameters including auth header - expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/posts', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer mock-jwt-token', + expect(fetchMock).toHaveBeenCalledWith( + "https://postcentral.hyf.dev/posts", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer mock-jwt-token", + }, + body: JSON.stringify({ text: postText }), }, - body: JSON.stringify({ text: postText }), - }); + ); expect(fetchMock).toHaveBeenCalledTimes(1); }); - it('should return post data with correct structure', async () => { - const postText = 'My test post!'; - const mockData = { id: 1, text: postText, user: 'Alice' }; + it("should return post data with correct structure", async () => { + const postText = "My test post!"; + const mockData = { id: 1, text: postText, user: "Alice" }; fetchMock.mockResolvedValueOnce({ ok: true, @@ -198,42 +201,42 @@ describe('POST Functions', () => { const data = await createPost(postText); // Verify the function returns the expected data structure - expect(data).toHaveProperty('id'); - expect(data).toHaveProperty('text'); - expect(data).toHaveProperty('user'); + expect(data).toHaveProperty("id"); + expect(data).toHaveProperty("text"); + expect(data).toHaveProperty("user"); expect(data.text).toBe(postText); - expect(data.user).toBe('Alice'); - expect(typeof data.id).toBe('number'); + expect(data.user).toBe("Alice"); + expect(typeof data.id).toBe("number"); }); - it('should include Content-Type and Authorization headers', async () => { + it("should include Content-Type and Authorization headers", async () => { fetchMock.mockResolvedValueOnce({ ok: true, status: 201, - json: async () => ({ id: 1, text: 'Test', user: 'Alice' }), + json: async () => ({ id: 1, text: "Test", user: "Alice" }), }); - await createPost('Test'); + await createPost("Test"); // Check that headers include both Content-Type and Authorization const callArgs = fetchMock.mock.calls[0][1]; - expect(callArgs.headers).toHaveProperty('Content-Type'); - expect(callArgs.headers['Content-Type']).toBe('application/json'); - expect(callArgs.headers).toHaveProperty('Authorization'); - expect(callArgs.headers['Authorization']).toBe('Bearer mock-jwt-token'); + expect(callArgs.headers).toHaveProperty("Content-Type"); + expect(callArgs.headers["Content-Type"]).toBe("application/json"); + expect(callArgs.headers).toHaveProperty("Authorization"); + expect(callArgs.headers["Authorization"]).toBe("Bearer mock-jwt-token"); }); - it('should throw error when not authenticated', async () => { + it("should throw error when not authenticated", async () => { // Mock 401 response (unauthorized - no token) fetchMock.mockResolvedValueOnce({ ok: false, status: 401, - statusText: 'Unauthorized', - json: async () => ({ error: 'Token required' }), + statusText: "Unauthorized", + json: async () => ({ error: "Token required" }), }); // Verify the function throws an error - await expect(createPost('Test post')).rejects.toThrow(); + await expect(createPost("Test post")).rejects.toThrow(); }); }); });