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..070190978 --- /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`, () => { + return 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..16cf59bf2 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,78 @@ 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: 'target', + 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 +124,47 @@ 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), ];