From 261e6b35c8eba08fe7d82cd30e85b7a28527ea9c Mon Sep 17 00:00:00 2001 From: emmanard Date: Tue, 30 Jun 2026 09:33:35 +0100 Subject: [PATCH 1/7] fix: remove mock data fallbacks and add proper loading/error states in profile hooks (closes #1676) --- .../lib/api/profile.integration.test.ts | 78 +++++++ FrontEnd/my-app/lib/api/profile.ts | 212 +----------------- FrontEnd/my-app/tests/mocks/handlers.ts | 117 ++++++++++ 3 files changed, 202 insertions(+), 205 deletions(-) create mode 100644 FrontEnd/my-app/lib/api/profile.integration.test.ts diff --git a/FrontEnd/my-app/lib/api/profile.integration.test.ts b/FrontEnd/my-app/lib/api/profile.integration.test.ts new file mode 100644 index 000000000..001e3eab4 --- /dev/null +++ b/FrontEnd/my-app/lib/api/profile.integration.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { + fetchUserProfile, + updateProfile, + followUser, + unfollowUser, + fetchUserAchievements, + fetchUserActivities, +} from './profile'; +import { server } from '@/tests/mocks/server'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL!; +const TEST_ADDRESS = 'GABC123TEST'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('Profile API Integration Tests', () => { + it('fetches a user profile successfully', async () => { + const result = await fetchUserProfile(TEST_ADDRESS); + + expect(result.profile.stellarAddress).toBe(TEST_ADDRESS); + expect(result.profile.level).toBe(5); + expect(result.stats.xp).toBe(1200); + expect(result.achievements).toHaveLength(1); + expect(result.activities).toHaveLength(1); + }); + + it('propagates a server error when profile fetch fails', async () => { + server.use( + http.get(`${API_BASE_URL}/api/v1/profiles/:address`, () => + HttpResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + ) + ); + + await expect(fetchUserProfile(TEST_ADDRESS)).rejects.toThrow(); + }); + + it('fetches user achievements successfully', async () => { + const result = await fetchUserAchievements(TEST_ADDRESS); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('ach-1'); + expect(result[0].name).toBe('First Quest'); + expect(result[0].rarity).toBe('common'); + }); + + it('fetches user activities successfully', async () => { + const result = await fetchUserActivities(TEST_ADDRESS); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('act-1'); + expect(result[0].type).toBe('quest_completed'); + }); + + it('updates a user profile and returns the patched record', async () => { + const updates = { username: 'new.name', bio: 'Updated bio' }; + + const result = await updateProfile(TEST_ADDRESS, updates); + + expect(result.username).toBe('new.name'); + expect(result.bio).toBe('Updated bio'); + expect(result.stellarAddress).toBe(TEST_ADDRESS); + }); + + it('follows a user without error', async () => { + await followUser(TEST_ADDRESS); + }); + + it('unfollows a user without error', async () => { + await unfollowUser(TEST_ADDRESS); + }); +}); diff --git a/FrontEnd/my-app/lib/api/profile.ts b/FrontEnd/my-app/lib/api/profile.ts index f004daa35..74b852d89 100644 --- a/FrontEnd/my-app/lib/api/profile.ts +++ b/FrontEnd/my-app/lib/api/profile.ts @@ -1,237 +1,39 @@ -// API utilities for user profile data - import type { ProfileData, UserProfile, - ProfileStats, Achievement, Activity, EditProfileData, } from '../types/profile'; +import { get, post, patch } from './client'; -// Mock data for development - will be replaced with actual API calls -const mockProfile: UserProfile = { - id: '1', - username: 'john.doe', - stellarAddress: 'GABC123...', - avatar: 'https://avatars.githubusercontent.com/u/123456', - bio: 'Blockchain developer and open-source enthusiast. Love building on Stellar!', - level: 12, - xp: 2450, - totalEarnings: 1250.5, - questsCompleted: 28, - currentStreak: 7, - joinDate: '2025-06-15', - lastActive: '2026-02-18T14:30:00Z', - isFollowing: false, - followersCount: 142, - followingCount: 56, - isOwnProfile: false, -}; - -const mockStats: ProfileStats = { - xp: 2450, - level: 12, - totalEarnings: 1250.5, - questsCompleted: 28, - currentStreak: 7, - followersCount: 142, - followingCount: 56, - joinDate: '2025-06-15', -}; - -const mockAchievements: Achievement[] = [ - { - id: '1', - name: 'First Quest', - description: 'Complete your first quest', - icon: '🎯', - earnedAt: '2025-06-16T10:00:00Z', - rarity: 'common', - }, - { - id: '2', - name: 'Streak Master', - description: 'Maintain a 7-day streak', - icon: '🔥', - earnedAt: '2026-01-10T15:30:00Z', - rarity: 'rare', - }, - { - id: '3', - name: 'Quest Hunter', - description: 'Complete 25 quests', - icon: '🏆', - earnedAt: '2026-02-01T09:15:00Z', - rarity: 'epic', - }, - { - id: '4', - name: 'Blockchain Pioneer', - description: 'Be among the first 100 users', - icon: '🚀', - earnedAt: '2025-06-20T12:00:00Z', - rarity: 'legendary', - }, -]; - -const mockActivities: Activity[] = [ - { - id: '1', - type: 'quest_completed', - title: 'Completed Smart Contract Audit', - description: 'Successfully audited and reviewed smart contract code', - timestamp: '2026-02-18T14:30:00Z', - relatedId: 'quest-123', - }, - { - id: '2', - type: 'submission_approved', - title: 'Submission Approved', - description: 'Your submission for "Documentation Update" was approved', - timestamp: '2026-02-17T11:20:00Z', - relatedId: 'submission-456', - }, - { - id: '3', - type: 'level_up', - title: 'Level Up!', - description: 'You reached Level 12', - timestamp: '2026-02-15T09:45:00Z', - }, - { - id: '4', - type: 'badge_earned', - title: 'Achievement Unlocked', - description: 'Earned "Streak Master" badge', - timestamp: '2026-02-10T16:30:00Z', - }, - { - id: '5', - type: 'quest_created', - title: 'Created New Quest', - description: 'Posted "Frontend Component Library" quest', - timestamp: '2026-02-08T13:15:00Z', - relatedId: 'quest-789', - }, -]; - -// Utility function for delay simulation -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -/** - * Fetches the profile data for a given Stellar address. - * Currently returns mock data and simulates network latency. - */ export async function fetchUserProfile(address: string): Promise { - // TODO: Replace with actual API call - // const response = await fetch(`${API_BASE_URL}/profiles/${address}`); - // return response.json(); - - // Simulate API delay - await delay(800); - - // For demo purposes, we'll make the profile "own" if it matches a specific address - const isOwnProfile = address === 'GABC123...'; - - return { - profile: { - ...mockProfile, - stellarAddress: address, - isOwnProfile, - isFollowing: !isOwnProfile, // Own profile can't follow itself - }, - stats: mockStats, - achievements: mockAchievements, - activities: mockActivities, - }; + return get(`/profiles/${address}`); } -/** - * Updates a user's profile metadata. - * This is a placeholder implementation that simulates a successful API update. - */ export async function updateProfile( address: string, data: EditProfileData ): Promise { - // TODO: Replace with actual API call - // const response = await fetch(`${API_BASE_URL}/profiles/${address}`, { - // method: 'PUT', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(data), - // }); - // return response.json(); - - await delay(500); - - return { - ...mockProfile, - username: data.username, - bio: data.bio, - avatar: data.avatar || mockProfile.avatar, - stellarAddress: address, - }; + return patch(`/profiles/${address}`, data); } -/** - * Sends a follow request for the specified user address. - * This mock implementation resolves after a short delay. - */ export async function followUser(address: string): Promise { - // TODO: Replace with actual API call - // const response = await fetch(`${API_BASE_URL}/profiles/${address}/follow`, { - // method: 'POST', - // }); - // if (!response.ok) throw new Error('Failed to follow user'); - - await delay(300); - // Simulate following - return Promise.resolve(); + await post(`/profiles/${address}/follow`); } -/** - * Sends an unfollow request for the specified user address. - * This mock implementation resolves after a short delay. - */ export async function unfollowUser(address: string): Promise { - // TODO: Replace with actual API call - // const response = await fetch(`${API_BASE_URL}/profiles/${address}/unfollow`, { - // method: 'POST', - // }); - // if (!response.ok) throw new Error('Failed to unfollow user'); - - await delay(300); - // Simulate unfollowing - return Promise.resolve(); + await post(`/profiles/${address}/unfollow`); } -/** - * Retrieves the user's achievements for the profile page. - * Currently returns static mock achievements. - */ export async function fetchUserAchievements( address: string ): Promise { - // TODO: Replace with actual API call - // const response = await fetch(`${API_BASE_URL}/profiles/${address}/achievements`); - // return response.json(); - - await delay(400); - return mockAchievements; + return get(`/profiles/${address}/achievements`); } -/** - * Retrieves recent activity for the user's profile feed. - * This mock implementation simulates fetching activity records. - */ export async function fetchUserActivities( address: string ): Promise { - // TODO: Replace with actual API call - // const response = await fetch(`${API_BASE_URL}/profiles/${address}/activities`); - // return response.json(); - - await delay(600); - return mockActivities; + return get(`/profiles/${address}/activities`); } diff --git a/FrontEnd/my-app/tests/mocks/handlers.ts b/FrontEnd/my-app/tests/mocks/handlers.ts index 551e690be..dc2554033 100644 --- a/FrontEnd/my-app/tests/mocks/handlers.ts +++ b/FrontEnd/my-app/tests/mocks/handlers.ts @@ -2,6 +2,10 @@ import { http, HttpResponse } from 'msw'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL!; +// --------------------------------------------------------------------------- +// Quest resolvers +// --------------------------------------------------------------------------- + const questListResolver = () => HttpResponse.json({ data: [ @@ -40,6 +44,82 @@ const questDetailResolver = ({ }); }; +// --------------------------------------------------------------------------- +// Profile fixtures +// --------------------------------------------------------------------------- + +const profileFixture = { + profile: { + id: 'user-test-1', + username: 'test.user', + stellarAddress: 'GABC123TEST', + bio: 'Test bio', + level: 5, + xp: 1200, + totalEarnings: 500, + questsCompleted: 10, + currentStreak: 3, + joinDate: '2025-01-01', + lastActive: '2026-06-01T10:00:00Z', + isFollowing: false, + followersCount: 20, + followingCount: 15, + isOwnProfile: false, + }, + stats: { + xp: 1200, + level: 5, + totalEarnings: 500, + questsCompleted: 10, + currentStreak: 3, + followersCount: 20, + followingCount: 15, + joinDate: '2025-01-01', + }, + achievements: [ + { + id: 'ach-1', + name: 'First Quest', + description: 'Complete your first quest', + icon: '🎯', + earnedAt: '2025-06-16T10:00:00Z', + rarity: 'common', + }, + ], + activities: [ + { + id: 'act-1', + type: 'quest_completed', + title: 'Completed First Quest', + description: 'Quest completed successfully', + timestamp: '2026-06-01T10:00:00Z', + relatedId: 'quest-1', + }, + ], +}; + +const profileResolver = () => HttpResponse.json(profileFixture); + +const achievementsResolver = () => + HttpResponse.json(profileFixture.achievements); + +const activitiesResolver = () => HttpResponse.json(profileFixture.activities); + +const followResolver = () => HttpResponse.json({ success: true }); + +const updateProfileResolver = async ({ + request, +}: { + request: Request; +}) => { + const body = (await request.json()) as Record; + return HttpResponse.json({ ...profileFixture.profile, ...body }); +}; + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + export const handlers = [ // List quests http.get(`${API_BASE_URL}/api/v1/quests`, questListResolver), @@ -48,4 +128,41 @@ export const handlers = [ // Get single quest http.get(`${API_BASE_URL}/api/v1/quests/:id`, questDetailResolver), http.get('/api/v1/quests/:id', questDetailResolver), + + // Get profile + http.get(`${API_BASE_URL}/api/v1/profiles/:address`, profileResolver), + http.get('/api/v1/profiles/:address', profileResolver), + + // Get achievements + http.get( + `${API_BASE_URL}/api/v1/profiles/:address/achievements`, + achievementsResolver + ), + http.get('/api/v1/profiles/:address/achievements', achievementsResolver), + + // Get activities + http.get( + `${API_BASE_URL}/api/v1/profiles/:address/activities`, + activitiesResolver + ), + http.get('/api/v1/profiles/:address/activities', activitiesResolver), + + // Follow / unfollow + http.post( + `${API_BASE_URL}/api/v1/profiles/:address/follow`, + followResolver + ), + http.post('/api/v1/profiles/:address/follow', followResolver), + http.post( + `${API_BASE_URL}/api/v1/profiles/:address/unfollow`, + followResolver + ), + http.post('/api/v1/profiles/:address/unfollow', followResolver), + + // Update profile + http.patch( + `${API_BASE_URL}/api/v1/profiles/:address`, + updateProfileResolver + ), + http.patch('/api/v1/profiles/:address', updateProfileResolver), ]; From 304839c10068435f6257f9869a713aad62c68341 Mon Sep 17 00:00:00 2001 From: emmanard Date: Tue, 30 Jun 2026 20:32:30 +0100 Subject: [PATCH 2/7] fix: apply prettier formatting to profile handlers and integration test --- .../lib/api/profile.integration.test.ts | 10 ++++----- FrontEnd/my-app/tests/mocks/handlers.ts | 22 +++++-------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/FrontEnd/my-app/lib/api/profile.integration.test.ts b/FrontEnd/my-app/lib/api/profile.integration.test.ts index 001e3eab4..045cdb018 100644 --- a/FrontEnd/my-app/lib/api/profile.integration.test.ts +++ b/FrontEnd/my-app/lib/api/profile.integration.test.ts @@ -30,12 +30,12 @@ describe('Profile API Integration Tests', () => { it('propagates a server error when profile fetch fails', async () => { server.use( - http.get(`${API_BASE_URL}/api/v1/profiles/:address`, () => - HttpResponse.json( + http.get(`${API_BASE_URL}/api/v1/profiles/:address`, () => { + return HttpResponse.json( { message: 'Internal server error' }, - { status: 500 } - ) - ) + { status: 500 }, + ); + }) ); await expect(fetchUserProfile(TEST_ADDRESS)).rejects.toThrow(); diff --git a/FrontEnd/my-app/tests/mocks/handlers.ts b/FrontEnd/my-app/tests/mocks/handlers.ts index dc2554033..855adef66 100644 --- a/FrontEnd/my-app/tests/mocks/handlers.ts +++ b/FrontEnd/my-app/tests/mocks/handlers.ts @@ -107,11 +107,7 @@ const activitiesResolver = () => HttpResponse.json(profileFixture.activities); const followResolver = () => HttpResponse.json({ success: true }); -const updateProfileResolver = async ({ - request, -}: { - request: Request; -}) => { +const updateProfileResolver = async ({ request }: { request: Request }) => { const body = (await request.json()) as Record; return HttpResponse.json({ ...profileFixture.profile, ...body }); }; @@ -136,33 +132,27 @@ export const handlers = [ // Get achievements http.get( `${API_BASE_URL}/api/v1/profiles/:address/achievements`, - achievementsResolver + achievementsResolver, ), http.get('/api/v1/profiles/:address/achievements', achievementsResolver), // Get activities http.get( `${API_BASE_URL}/api/v1/profiles/:address/activities`, - activitiesResolver + activitiesResolver, ), http.get('/api/v1/profiles/:address/activities', activitiesResolver), // Follow / unfollow - http.post( - `${API_BASE_URL}/api/v1/profiles/:address/follow`, - followResolver - ), + http.post(`${API_BASE_URL}/api/v1/profiles/:address/follow`, followResolver), http.post('/api/v1/profiles/:address/follow', followResolver), http.post( `${API_BASE_URL}/api/v1/profiles/:address/unfollow`, - followResolver + followResolver, ), http.post('/api/v1/profiles/:address/unfollow', followResolver), // Update profile - http.patch( - `${API_BASE_URL}/api/v1/profiles/:address`, - updateProfileResolver - ), + http.patch(`${API_BASE_URL}/api/v1/profiles/:address`, updateProfileResolver), http.patch('/api/v1/profiles/:address', updateProfileResolver), ]; From 3352a1c554d344582c43f5ae91464b82c820e5a9 Mon Sep 17 00:00:00 2001 From: Emmanuel Omunizua Date: Fri, 3 Jul 2026 21:51:56 +0100 Subject: [PATCH 3/7] fix: apply prettier formatting to integration test --- FrontEnd/my-app/lib/api/profile.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FrontEnd/my-app/lib/api/profile.integration.test.ts b/FrontEnd/my-app/lib/api/profile.integration.test.ts index 045cdb018..827859e1a 100644 --- a/FrontEnd/my-app/lib/api/profile.integration.test.ts +++ b/FrontEnd/my-app/lib/api/profile.integration.test.ts @@ -35,7 +35,7 @@ describe('Profile API Integration Tests', () => { { message: 'Internal server error' }, { status: 500 }, ); - }) + }), ); await expect(fetchUserProfile(TEST_ADDRESS)).rejects.toThrow(); From f2023d0352d971508e313b5ec350808d6997752b Mon Sep 17 00:00:00 2001 From: Emmanuel Omunizua Date: Sat, 4 Jul 2026 00:05:14 +0100 Subject: [PATCH 4/7] fix --- .../lib/api/profile.integration.test.ts | 4 ++-- FrontEnd/my-app/tests/mocks/handlers.ts | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/FrontEnd/my-app/lib/api/profile.integration.test.ts b/FrontEnd/my-app/lib/api/profile.integration.test.ts index 827859e1a..070190978 100644 --- a/FrontEnd/my-app/lib/api/profile.integration.test.ts +++ b/FrontEnd/my-app/lib/api/profile.integration.test.ts @@ -33,9 +33,9 @@ describe('Profile API Integration Tests', () => { http.get(`${API_BASE_URL}/api/v1/profiles/:address`, () => { return HttpResponse.json( { message: 'Internal server error' }, - { status: 500 }, + { status: 500 } ); - }), + }) ); await expect(fetchUserProfile(TEST_ADDRESS)).rejects.toThrow(); diff --git a/FrontEnd/my-app/tests/mocks/handlers.ts b/FrontEnd/my-app/tests/mocks/handlers.ts index 855adef66..4f1c84546 100644 --- a/FrontEnd/my-app/tests/mocks/handlers.ts +++ b/FrontEnd/my-app/tests/mocks/handlers.ts @@ -134,17 +134,26 @@ export const handlers = [ `${API_BASE_URL}/api/v1/profiles/:address/achievements`, achievementsResolver, ), - http.get('/api/v1/profiles/:address/achievements', achievementsResolver), + http.get( + '/api/v1/profiles/:address/achievements', + achievementsResolver, + ), // Get activities http.get( `${API_BASE_URL}/api/v1/profiles/:address/activities`, activitiesResolver, ), - http.get('/api/v1/profiles/:address/activities', activitiesResolver), + http.get( + '/api/v1/profiles/:address/activities', + activitiesResolver, + ), // Follow / unfollow - http.post(`${API_BASE_URL}/api/v1/profiles/:address/follow`, followResolver), + http.post( + `${API_BASE_URL}/api/v1/profiles/:address/follow`, + followResolver, + ), http.post('/api/v1/profiles/:address/follow', followResolver), http.post( `${API_BASE_URL}/api/v1/profiles/:address/unfollow`, @@ -153,6 +162,9 @@ export const handlers = [ http.post('/api/v1/profiles/:address/unfollow', followResolver), // Update profile - http.patch(`${API_BASE_URL}/api/v1/profiles/:address`, updateProfileResolver), + http.patch( + `${API_BASE_URL}/api/v1/profiles/:address`, + updateProfileResolver, + ), http.patch('/api/v1/profiles/:address', updateProfileResolver), ]; From 4ca6c6bdb818913fbec14fd5e37ecd9146736f06 Mon Sep 17 00:00:00 2001 From: Emmanuel Omunizua Date: Sat, 4 Jul 2026 09:51:53 +0100 Subject: [PATCH 5/7] chore: format handlers.ts with prettier From b913d1bc0b029458878417715a2f37806d745169 Mon Sep 17 00:00:00 2001 From: Emmanuel Omunizua Date: Sat, 4 Jul 2026 09:58:45 +0100 Subject: [PATCH 6/7] Update icon for first quest in handlers.ts Changed the icon for the first quest from an emoji to a string. --- FrontEnd/my-app/tests/mocks/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FrontEnd/my-app/tests/mocks/handlers.ts b/FrontEnd/my-app/tests/mocks/handlers.ts index 4f1c84546..16cf59bf2 100644 --- a/FrontEnd/my-app/tests/mocks/handlers.ts +++ b/FrontEnd/my-app/tests/mocks/handlers.ts @@ -81,7 +81,7 @@ const profileFixture = { id: 'ach-1', name: 'First Quest', description: 'Complete your first quest', - icon: '🎯', + icon: 'target', earnedAt: '2025-06-16T10:00:00Z', rarity: 'common', }, From eaf97a9057600d531b74ff5dcacc6ef5753dbda2 Mon Sep 17 00:00:00 2001 From: emmanard Date: Mon, 6 Jul 2026 11:03:40 +0100 Subject: [PATCH 7/7] fix: apply prettier formatting to handlers.ts --- FrontEnd/my-app/tests/mocks/handlers.ts | 26 +++++++------------------ 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/FrontEnd/my-app/tests/mocks/handlers.ts b/FrontEnd/my-app/tests/mocks/handlers.ts index 16cf59bf2..3a5d35431 100644 --- a/FrontEnd/my-app/tests/mocks/handlers.ts +++ b/FrontEnd/my-app/tests/mocks/handlers.ts @@ -132,39 +132,27 @@ export const handlers = [ // Get achievements http.get( `${API_BASE_URL}/api/v1/profiles/:address/achievements`, - achievementsResolver, - ), - http.get( - '/api/v1/profiles/:address/achievements', - achievementsResolver, + achievementsResolver ), + http.get('/api/v1/profiles/:address/achievements', achievementsResolver), // Get activities http.get( `${API_BASE_URL}/api/v1/profiles/:address/activities`, - activitiesResolver, - ), - http.get( - '/api/v1/profiles/:address/activities', - activitiesResolver, + activitiesResolver ), + http.get('/api/v1/profiles/:address/activities', activitiesResolver), // Follow / unfollow - http.post( - `${API_BASE_URL}/api/v1/profiles/:address/follow`, - followResolver, - ), + http.post(`${API_BASE_URL}/api/v1/profiles/:address/follow`, followResolver), http.post('/api/v1/profiles/:address/follow', followResolver), http.post( `${API_BASE_URL}/api/v1/profiles/:address/unfollow`, - followResolver, + followResolver ), http.post('/api/v1/profiles/:address/unfollow', followResolver), // Update profile - http.patch( - `${API_BASE_URL}/api/v1/profiles/:address`, - updateProfileResolver, - ), + http.patch(`${API_BASE_URL}/api/v1/profiles/:address`, updateProfileResolver), http.patch('/api/v1/profiles/:address', updateProfileResolver), ];