diff --git a/client/lib/onboarding.integration.spec.ts b/client/lib/onboarding.integration.spec.ts new file mode 100644 index 00000000..932a1934 --- /dev/null +++ b/client/lib/onboarding.integration.spec.ts @@ -0,0 +1,148 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +const createStorage = () => { + const store = new Map(); + const storage: Storage = { + get length() { + return store.size; + }, + clear: () => { + store.clear(); + }, + getItem: (key: string) => store.get(key) ?? null, + key: (index: number) => Array.from(store.keys())[index] ?? null, + removeItem: (key: string) => { + store.delete(key); + }, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + } as Storage; + return storage; +}; + +vi.stubGlobal("localStorage", createStorage()); +vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + status: 404, + text: async () => "", + json: async () => ({}), + }), +); + +vi.mock("@/lib/supabase", () => { + const chain = () => ({ + select: () => chain(), + insert: () => ({ data: null, error: null }), + update: () => ({ data: null, error: null }), + delete: () => ({ error: null }), + eq: () => chain(), + order: () => chain(), + limit: () => chain(), + single: () => ({ data: null, error: null }), + }); + + return { + supabase: { + auth: { + signInWithPassword: vi.fn(), + signInWithOAuth: vi.fn(), + linkIdentity: vi.fn(), + unlinkIdentity: vi.fn(), + signOut: vi.fn(), + getUser: async () => ({ data: { user: null }, error: null }), + getSession: async () => ({ data: { session: null }, error: null }), + onAuthStateChange: () => ({ + data: { + subscription: { + unsubscribe: () => {}, + }, + }, + }), + }, + from: () => chain(), + channel: () => ({ + on: () => ({}), + subscribe: () => ({ unsubscribe: () => {} }), + unsubscribe: () => {}, + }), + storage: { + from: () => ({ + upload: async () => ({ data: null, error: null }), + getPublicUrl: () => ({ data: { publicUrl: "" } }), + }), + }, + }, + isSupabaseConfigured: false, + }; +}); + +vi.mock("@/lib/aethex-toast", () => ({ + aethexToast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + aethex: vi.fn(), + system: vi.fn(), + }, +})); + +import { mockAuth } from "@/lib/mock-auth"; +import { + aethexUserService, + aethexAchievementService, + checkProfileComplete, + type AethexUserProfile, +} from "@/lib/aethex-database-adapter"; + +const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe("onboarding passport flow", () => { + beforeEach(async () => { + localStorage.clear(); + await mockAuth.signOut(); + (fetch as unknown as vi.Mock).mockReset(); + (fetch as unknown as vi.Mock).mockResolvedValue({ + ok: false, + status: 404, + text: async () => "", + json: async () => ({}), + }); + }); + + it("persists profile setup, links providers, and awards welcome badge", async () => { + const email = `tester+${Date.now()}@example.com`; + const password = "Secret123!"; + + const authResult = await mockAuth.signInWithPassword(email, password); + expect(authResult.error).toBeNull(); + await wait(); + + const user = authResult.data.user; + expect(user).toBeTruthy(); + + await aethexUserService.updateProfile(user.id, { + id: user.id, + username: "tester", + full_name: "Tester One", + user_type: "community_member" as AethexUserProfile["user_type"], + experience_level: "intermediate" as AethexUserProfile["experience_level"], + bio: "Building awesome experiences", + }); + + const hydratedProfile = await aethexUserService.getCurrentUser(); + expect(checkProfileComplete(hydratedProfile)).toBe(true); + + await mockAuth.linkIdentity({ provider: "github" }); + const refreshedUser = (await mockAuth.getUser()).data.user; + expect(refreshedUser?.identities?.some((id: any) => id.provider === "github")).toBe(true); + + await aethexAchievementService.checkAndAwardOnboardingAchievement(user.id); + const achievements = await aethexAchievementService.getUserAchievements(user.id); + const welcomeBadge = achievements.find((item) => item.name === "Welcome to AeThex"); + expect(welcomeBadge).toBeTruthy(); + }); +});