diff --git a/src/__tests__/helpers/updateEmoteSetMocks.ts b/src/__tests__/helpers/updateEmoteSetMocks.ts new file mode 100644 index 00000000..c4ef347f --- /dev/null +++ b/src/__tests__/helpers/updateEmoteSetMocks.ts @@ -0,0 +1,108 @@ +import { vi } from 'vitest' + +// Mock the middleware +vi.mock('@/lib/api-middlewares/with-authentication', () => ({ + withAuthentication: (handler) => handler, +})) + +// Mock prisma +vi.mock('@/lib/db', () => ({ + default: { + subscription: { + findMany: vi.fn(), + }, + }, +})) + +// Mock next-auth +vi.mock('next-auth', () => ({ + getServerSession: vi.fn(), +})) + +// Mock auth options +vi.mock('@/lib/auth', () => ({ + authOptions: {}, +})) + +// Mock the 7TV library functions +vi.mock('@/lib/7tv', () => ({ + get7TVUser: vi.fn(), + getOrCreateEmoteSet: vi.fn(), +})) + +// Prepare variable to capture GraphQL requests +export let mockRequest: ReturnType + +// Mock GraphQL client and gql +vi.mock('graphql-request', () => { + mockRequest = vi.fn() + return { + GraphQLClient: vi.fn().mockImplementation(() => ({ + request: mockRequest, + })), + gql: (query: string) => query, + } +}) + +// Mock authentication +vi.mock('@/lib/api/getServerSession', () => ({ + getServerSession: vi.fn(), +})) + +// Mock subscription utils +vi.mock('@/utils/subscription', () => ({ + getSubscription: vi.fn(), + canAccessFeature: vi.fn(), + SubscriptionTier: { + FREE: 'FREE', + PRO: 'PRO', + }, +})) + +// Mock ChatBot emotes +vi.mock('@/components/Dashboard/ChatBot', () => ({ + emotesRequired: [ + { id: 'emote1', label: 'Emote1' }, + { id: 'emote2', label: 'Emote2' }, + ], +})) + +// Mock getTwitchTokens to avoid environment variable issues +vi.mock('@/lib/getTwitchTokens', () => ({ + default: vi.fn().mockResolvedValue({ + access_token: 'mock-access-token', + expires_in: 14400, + token_type: 'bearer', + }), + CLIENT_ID: 'mock-client-id', + CLIENT_SECRET: 'mock-client-secret', +})) + +// Import mocked modules so tests can use them +import { get7TVUser, getOrCreateEmoteSet } from '@/lib/7tv' +import { getServerSession } from '@/lib/api/getServerSession' +import { canAccessFeature, getSubscription } from '@/utils/subscription' +import { GraphQLClient } from 'graphql-request' + +export { + get7TVUser, + getOrCreateEmoteSet, + getServerSession, + canAccessFeature, + getSubscription, + GraphQLClient, +} + +export function setupEnv() { + vi.stubEnv('SEVENTV_AUTH', 'test-auth-token') + vi.stubEnv('TWITCH_CLIENT_ID', 'mock-client-id') + vi.stubEnv('TWITCH_CLIENT_SECRET', 'mock-client-secret') + mockRequest = vi.fn() + vi.mocked(GraphQLClient).mockImplementation( + () => ({ request: mockRequest }) as unknown as GraphQLClient, + ) +} + +export function cleanupEnv() { + vi.unstubAllEnvs() +} diff --git a/src/__tests__/pages/api/update-emote-set/access.test.ts b/src/__tests__/pages/api/update-emote-set/access.test.ts new file mode 100644 index 00000000..313f9bc3 --- /dev/null +++ b/src/__tests__/pages/api/update-emote-set/access.test.ts @@ -0,0 +1,102 @@ +import handler from '@/pages/api/update-emote-set' +import { createMocks } from 'node-mocks-http' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + setupEnv, + cleanupEnv, + getServerSession, + getSubscription, + canAccessFeature, +} from '../../helpers/updateEmoteSetMocks' + +// Load shared mocks +import '../../helpers/updateEmoteSetMocks' + +describe('update-emote-set API - access control', () => { + beforeEach(() => { + vi.resetAllMocks() + setupEnv() + }) + + afterEach(() => { + cleanupEnv() + }) + + it('returns 403 when user is impersonating', async () => { + vi.mocked(getServerSession).mockResolvedValueOnce({ + user: { + id: 'user-123', + isImpersonating: true, + name: 'Test User', + image: 'image-url', + twitchId: 'twitch-123', + role: 'user', + locale: 'en-US', + scope: 'test-scope', + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + + const { req, res } = createMocks({ method: 'GET' }) + + await handler(req, res) + + expect(res.statusCode).toBe(403) + expect(res._getJSONData()).toEqual({ message: 'Forbidden' }) + }) + + it('returns 403 when user is not authenticated', async () => { + vi.mocked(getServerSession).mockResolvedValueOnce(null) + + const { req, res } = createMocks({ method: 'GET' }) + + await handler(req, res) + + expect(res.statusCode).toBe(403) + expect(res._getJSONData()).toEqual({ message: 'Forbidden' }) + }) + + it('returns 403 when user does not have access to auto7TV feature', async () => { + vi.mocked(getServerSession).mockResolvedValueOnce({ + user: { + id: 'user-123', + name: 'Test User', + image: 'image-url', + isImpersonating: false, + twitchId: 'twitch-123', + role: 'user', + locale: 'en-US', + scope: 'test-scope', + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + + vi.mocked(getSubscription).mockResolvedValueOnce({ + id: 'subscription-123', + tier: 'FREE', + stripeCustomerId: null, + stripePriceId: null, + stripeSubscriptionId: null, + status: 'ACTIVE', + currentPeriodEnd: null, + cancelAtPeriodEnd: false, + isGift: false, + transactionType: 'RECURRING', + createdAt: new Date(), + giftDetails: null, + metadata: {}, + }) + + vi.mocked(canAccessFeature).mockReturnValueOnce({ + hasAccess: false, + requiredTier: 'PRO', + }) + + const { req, res } = createMocks({ method: 'GET' }) + + await handler(req, res) + + expect(res.statusCode).toBe(403) + expect(res._getJSONData()).toEqual({ message: 'Forbidden' }) + }) +}) diff --git a/src/__tests__/pages/api/update-emote-set.test.ts b/src/__tests__/pages/api/update-emote-set/integration.test.ts similarity index 51% rename from src/__tests__/pages/api/update-emote-set.test.ts rename to src/__tests__/pages/api/update-emote-set/integration.test.ts index ffa95611..ef585638 100644 --- a/src/__tests__/pages/api/update-emote-set.test.ts +++ b/src/__tests__/pages/api/update-emote-set/integration.test.ts @@ -1,347 +1,28 @@ import handler from '@/pages/api/update-emote-set' import { createMocks } from 'node-mocks-http' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -// Mock the middleware -vi.mock('@/lib/api-middlewares/with-authentication', () => ({ - withAuthentication: (handler) => handler, -})) - -// Mock prisma -vi.mock('@/lib/db', () => ({ - default: { - subscription: { - findMany: vi.fn(), - }, - }, -})) - -// Mock next-auth -vi.mock('next-auth', () => ({ - getServerSession: vi.fn(), -})) - -// Mock auth options -vi.mock('@/lib/auth', () => ({ - authOptions: {}, -})) - -// Mock the 7TV library functions -vi.mock('@/lib/7tv', () => ({ - get7TVUser: vi.fn(), - getOrCreateEmoteSet: vi.fn(), -})) - -// Mock GraphQL client and gql -vi.mock('graphql-request', () => { - const mockRequest = vi.fn() - return { - GraphQLClient: vi.fn().mockImplementation(() => ({ - request: mockRequest, - })), - gql: (query) => query, - } -}) - -// Mock authentication -vi.mock('@/lib/api/getServerSession', () => ({ - getServerSession: vi.fn(), -})) - -// Mock subscription utils -vi.mock('@/utils/subscription', () => ({ - getSubscription: vi.fn(), - canAccessFeature: vi.fn(), - SubscriptionTier: { - FREE: 'FREE', - PRO: 'PRO', - }, -})) - -// Mock ChatBot emotes -vi.mock('@/components/Dashboard/ChatBot', () => ({ - emotesRequired: [ - { id: 'emote1', label: 'Emote1' }, - { id: 'emote2', label: 'Emote2' }, - ], -})) - -// Mock getTwitchTokens to avoid environment variable issues -vi.mock('@/lib/getTwitchTokens', () => ({ - default: vi.fn().mockResolvedValue({ - access_token: 'mock-access-token', - expires_in: 14400, - token_type: 'bearer', - }), - CLIENT_ID: 'mock-client-id', - CLIENT_SECRET: 'mock-client-secret', -})) - -// Import mocked modules -import { get7TVUser, getOrCreateEmoteSet } from '@/lib/7tv' -import { getServerSession } from '@/lib/api/getServerSession' -import { canAccessFeature, getSubscription } from '@/utils/subscription' -import { GraphQLClient } from 'graphql-request' - -describe('update-emote-set API', () => { - let mockRequest: ReturnType - +import { + setupEnv, + cleanupEnv, + getServerSession, + getSubscription, + canAccessFeature, + get7TVUser, + getOrCreateEmoteSet, + mockRequest, +} from '../../helpers/updateEmoteSetMocks' + +import '../../helpers/updateEmoteSetMocks' + +describe('update-emote-set API - 7TV integration', () => { beforeEach(() => { vi.resetAllMocks() - - // Setup environment variables for testing - vi.stubEnv('SEVENTV_AUTH', 'test-auth-token') - vi.stubEnv('TWITCH_CLIENT_ID', 'mock-client-id') - vi.stubEnv('TWITCH_CLIENT_SECRET', 'mock-client-secret') - - // Reset the mock request function - mockRequest = vi.fn() - vi.mocked(GraphQLClient).mockImplementation( - () => - ({ - request: mockRequest, - }) as unknown as GraphQLClient, - ) + setupEnv() }) afterEach(() => { - // Restore original environment - vi.unstubAllEnvs() - }) - - it('returns 403 when user is impersonating', async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ - user: { - id: 'user-123', - isImpersonating: true, - name: 'Test User', - image: 'image-url', - twitchId: 'twitch-123', - role: 'user', - locale: 'en-US', - scope: 'test-scope', - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }) - - const { req, res } = createMocks({ - method: 'GET', - }) - - await handler(req, res) - - expect(res.statusCode).toBe(403) - expect(res._getJSONData()).toEqual({ message: 'Forbidden' }) - }) - - it('returns 403 when user is not authenticated', async () => { - vi.mocked(getServerSession).mockResolvedValueOnce(null) - - const { req, res } = createMocks({ - method: 'GET', - }) - - await handler(req, res) - - expect(res.statusCode).toBe(403) - expect(res._getJSONData()).toEqual({ message: 'Forbidden' }) - }) - - it('returns 403 when user does not have access to auto7TV feature', async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ - user: { - id: 'user-123', - name: 'Test User', - image: 'image-url', - isImpersonating: false, - twitchId: 'twitch-123', - role: 'user', - locale: 'en-US', - scope: 'test-scope', - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }) - - vi.mocked(getSubscription).mockResolvedValueOnce({ - id: 'subscription-123', - tier: 'FREE', - stripeCustomerId: null, - stripePriceId: null, - stripeSubscriptionId: null, - status: 'ACTIVE', - currentPeriodEnd: null, - cancelAtPeriodEnd: false, - isGift: false, - transactionType: 'RECURRING', - createdAt: new Date(), - giftDetails: null, - metadata: {}, - }) - - vi.mocked(canAccessFeature).mockReturnValueOnce({ - hasAccess: false, - requiredTier: 'PRO', - }) - - const { req, res } = createMocks({ - method: 'GET', - }) - - await handler(req, res) - - expect(res.statusCode).toBe(403) - expect(res._getJSONData()).toEqual({ message: 'Forbidden' }) - }) - - it('returns 400 when Twitch ID is missing', async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ - user: { - id: 'user-123', - name: 'Test User', - image: 'image-url', - isImpersonating: false, - twitchId: '', - role: 'user', - locale: 'en-US', - scope: 'test-scope', - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }) - - vi.mocked(getSubscription).mockResolvedValueOnce({ - id: 'subscription-123', - tier: 'PRO', - stripeCustomerId: null, - stripePriceId: null, - stripeSubscriptionId: null, - status: 'ACTIVE', - currentPeriodEnd: null, - cancelAtPeriodEnd: false, - isGift: false, - transactionType: 'RECURRING', - createdAt: new Date(), - giftDetails: null, - metadata: {}, - }) - - vi.mocked(canAccessFeature).mockReturnValueOnce({ - hasAccess: true, - requiredTier: 'PRO', - }) - - const { req, res } = createMocks({ - method: 'GET', - }) - - await handler(req, res) - - expect(res.statusCode).toBe(400) - expect(res._getJSONData()).toEqual({ message: 'Twitch ID is required' }) - }) - - it('returns 400 when emotesRequired is not defined', async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ - user: { - id: 'user-123', - twitchId: 'twitch-123', - name: 'Test User', - image: 'image-url', - isImpersonating: false, - role: 'user', - locale: 'en-US', - scope: 'test-scope', - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }) - - vi.mocked(getSubscription).mockResolvedValueOnce({ - id: 'subscription-123', - tier: 'PRO', - stripeCustomerId: null, - stripePriceId: null, - stripeSubscriptionId: null, - status: 'ACTIVE', - currentPeriodEnd: null, - cancelAtPeriodEnd: false, - isGift: false, - transactionType: 'RECURRING', - createdAt: new Date(), - giftDetails: null, - metadata: {}, - }) - - vi.mocked(canAccessFeature).mockReturnValueOnce({ - hasAccess: true, - requiredTier: 'PRO', - }) - - // Mock the ChatBot module - vi.mock('@/components/Dashboard/ChatBot', () => ({ - emotesRequired: [], - })) - - const { req, res } = createMocks({ - method: 'GET', - }) - - await handler(req, res) - - expect(res.statusCode).toBe(400) - expect(res._getJSONData()).toEqual({ message: 'No emotes defined for addition' }) + cleanupEnv() }) - - it('returns 500 when SEVENTV_AUTH is not set', async () => { - vi.mocked(getServerSession).mockResolvedValueOnce({ - user: { - id: 'user-123', - twitchId: 'twitch-123', - name: 'Test User', - image: 'image-url', - isImpersonating: false, - role: 'user', - locale: 'en-US', - scope: 'test-scope', - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }) - - vi.mocked(getSubscription).mockResolvedValueOnce({ - id: 'subscription-123', - tier: 'PRO', - stripeCustomerId: null, - stripePriceId: null, - stripeSubscriptionId: null, - status: 'ACTIVE', - currentPeriodEnd: null, - cancelAtPeriodEnd: false, - isGift: false, - transactionType: 'RECURRING', - createdAt: new Date(), - giftDetails: null, - metadata: {}, - }) - - vi.mocked(canAccessFeature).mockReturnValueOnce({ - hasAccess: true, - requiredTier: 'PRO', - }) - - // Remove SEVENTV_AUTH - vi.stubEnv('SEVENTV_AUTH', '') - - const { req, res } = createMocks({ - method: 'GET', - }) - - await handler(req, res) - - // Restore original value - vi.unstubAllEnvs() - - expect(res.statusCode).toBe(400) - expect(res._getJSONData()).toEqual({ message: 'No emotes defined for addition' }) - }) - it('returns 404 when 7TV user is not found', async () => { vi.mocked(getServerSession).mockResolvedValueOnce({ user: { diff --git a/src/__tests__/pages/api/update-emote-set/validation.test.ts b/src/__tests__/pages/api/update-emote-set/validation.test.ts new file mode 100644 index 00000000..ccbbd0b3 --- /dev/null +++ b/src/__tests__/pages/api/update-emote-set/validation.test.ts @@ -0,0 +1,163 @@ +import handler from '@/pages/api/update-emote-set' +import { createMocks } from 'node-mocks-http' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + setupEnv, + cleanupEnv, + getServerSession, + getSubscription, + canAccessFeature, +} from '../../helpers/updateEmoteSetMocks' + +import '../../helpers/updateEmoteSetMocks' + +describe('update-emote-set API - validation', () => { + beforeEach(() => { + vi.resetAllMocks() + setupEnv() + }) + + afterEach(() => { + cleanupEnv() + }) + + it('returns 400 when Twitch ID is missing', async () => { + vi.mocked(getServerSession).mockResolvedValueOnce({ + user: { + id: 'user-123', + name: 'Test User', + image: 'image-url', + isImpersonating: false, + twitchId: '', + role: 'user', + locale: 'en-US', + scope: 'test-scope', + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + + vi.mocked(getSubscription).mockResolvedValueOnce({ + id: 'subscription-123', + tier: 'PRO', + stripeCustomerId: null, + stripePriceId: null, + stripeSubscriptionId: null, + status: 'ACTIVE', + currentPeriodEnd: null, + cancelAtPeriodEnd: false, + isGift: false, + transactionType: 'RECURRING', + createdAt: new Date(), + giftDetails: null, + metadata: {}, + }) + + vi.mocked(canAccessFeature).mockReturnValueOnce({ + hasAccess: true, + requiredTier: 'PRO', + }) + + const { req, res } = createMocks({ method: 'GET' }) + + await handler(req, res) + + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toEqual({ message: 'Twitch ID is required' }) + }) + + it('returns 400 when emotesRequired is not defined', async () => { + vi.mocked(getServerSession).mockResolvedValueOnce({ + user: { + id: 'user-123', + twitchId: 'twitch-123', + name: 'Test User', + image: 'image-url', + isImpersonating: false, + role: 'user', + locale: 'en-US', + scope: 'test-scope', + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + + vi.mocked(getSubscription).mockResolvedValueOnce({ + id: 'subscription-123', + tier: 'PRO', + stripeCustomerId: null, + stripePriceId: null, + stripeSubscriptionId: null, + status: 'ACTIVE', + currentPeriodEnd: null, + cancelAtPeriodEnd: false, + isGift: false, + transactionType: 'RECURRING', + createdAt: new Date(), + giftDetails: null, + metadata: {}, + }) + + vi.mocked(canAccessFeature).mockReturnValueOnce({ + hasAccess: true, + requiredTier: 'PRO', + }) + + vi.mock('@/components/Dashboard/ChatBot', () => ({ + emotesRequired: [], + })) + + const { req, res } = createMocks({ method: 'GET' }) + + await handler(req, res) + + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toEqual({ message: 'No emotes defined for addition' }) + }) + + it('returns 500 when SEVENTV_AUTH is not set', async () => { + vi.mocked(getServerSession).mockResolvedValueOnce({ + user: { + id: 'user-123', + twitchId: 'twitch-123', + name: 'Test User', + image: 'image-url', + isImpersonating: false, + role: 'user', + locale: 'en-US', + scope: 'test-scope', + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + + vi.mocked(getSubscription).mockResolvedValueOnce({ + id: 'subscription-123', + tier: 'PRO', + stripeCustomerId: null, + stripePriceId: null, + stripeSubscriptionId: null, + status: 'ACTIVE', + currentPeriodEnd: null, + cancelAtPeriodEnd: false, + isGift: false, + transactionType: 'RECURRING', + createdAt: new Date(), + giftDetails: null, + metadata: {}, + }) + + vi.mocked(canAccessFeature).mockReturnValueOnce({ + hasAccess: true, + requiredTier: 'PRO', + }) + + vi.stubEnv('SEVENTV_AUTH', '') + + const { req, res } = createMocks({ method: 'GET' }) + + await handler(req, res) + + vi.unstubAllEnvs() + + expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toEqual({ message: 'No emotes defined for addition' }) + }) +})