Skip to content

Commit ef8be68

Browse files
committed
fix: review feedback for dev mock fallbacks
- Fix mock speaker surname null that rendered "null" in the UI - Extract shared isDevelopment helper to avoid duplication - Add unit tests for getUpcomingEvents and getSpeakers fallback logic - Update PR description to cover speakers changes
1 parent ae1bc0e commit ef8be68

7 files changed

Lines changed: 262 additions & 7 deletions

File tree

src/app/events/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EmptyEventsAlert } from '@/components/EmptyEventsAlert';
77
import { getUpcomingEvents } from '@/utils/getUpcomingEvents';
88
import { changeCityName } from '@/utils/changeCityName';
99
import { MOCK_PAST_EVENTS } from '@/utils/eventsMock';
10+
import { isDevelopment } from '@/utils/isDevelopment';
1011
import { ADDITIONAL_EVENTS } from '@/content/additionalEvents';
1112
import { filterUpcomingEvents, sortEventsByDate } from '@/utils/eventUtils';
1213
import { getTranslate } from '@/tolgee/server';
@@ -31,7 +32,7 @@ const getPastEvents = async () => {
3132

3233
if (!pastEventsRes.ok) {
3334
const body = await pastEventsRes.text();
34-
if (process.env.NODE_ENV !== 'production') {
35+
if (isDevelopment()) {
3536
console.warn(
3637
`[getPastEvents] API returned ${pastEventsRes.status} ${pastEventsRes.statusText}, using mock past events for development.`,
3738
);
@@ -48,7 +49,7 @@ const getPastEvents = async () => {
4849
const pastEvents = Object.values(data ?? {}).map(changeCityName);
4950
return sortEventsByDate(pastEvents, false); // false = descending order
5051
} catch (error) {
51-
if (process.env.NODE_ENV !== 'production') {
52+
if (isDevelopment()) {
5253
console.warn(
5354
'[getPastEvents] Failed to fetch or parse past events, using mock data for development:',
5455
error,

src/utils/getSpeakers.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { MOCK_SPEAKERS } from '@/utils/speakersMock';
3+
import type { SpeakerType } from '@/types/speaker';
4+
5+
const createFetchResponse = (overrides: Partial<Response> = {}): Response =>
6+
({
7+
ok: true,
8+
status: 200,
9+
statusText: 'OK',
10+
json: () => Promise.resolve([]),
11+
text: () => Promise.resolve(''),
12+
...overrides,
13+
}) as Response;
14+
15+
const mockEnv = (token: string): { env: Record<string, string> } => ({
16+
env: {
17+
SPEAKERS_API_URL: 'https://api.example.com/speakers',
18+
SPEAKERS_API_TOKEN: token,
19+
},
20+
});
21+
22+
describe('getSpeakers', () => {
23+
beforeEach(() => {
24+
vi.resetModules();
25+
vi.stubEnv('NODE_ENV', 'development');
26+
});
27+
28+
afterEach(() => {
29+
vi.unstubAllEnvs();
30+
vi.restoreAllMocks();
31+
});
32+
33+
it('returns parsed API data on success', async () => {
34+
const apiSpeaker: SpeakerType = {
35+
id: 1,
36+
name: 'John',
37+
surname: 'Doe',
38+
slug: 'john-doe',
39+
image: 'https://example.com/john.jpg',
40+
events_count: 5,
41+
url: 'https://example.com',
42+
};
43+
44+
global.fetch = vi.fn(() =>
45+
Promise.resolve(
46+
createFetchResponse({
47+
json: () => Promise.resolve([apiSpeaker]),
48+
}),
49+
),
50+
);
51+
52+
vi.doMock('@/env', () => mockEnv('real-token'));
53+
54+
const { getSpeakers } = await import('@/utils/getSpeakers');
55+
const result = await getSpeakers();
56+
57+
expect(result).toHaveLength(1);
58+
expect(result[0]?.name).toBe('John');
59+
});
60+
61+
it('returns mock speakers when token is placeholder in development', async () => {
62+
vi.doMock('@/env', () => mockEnv('<your_token>'));
63+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
64+
65+
const { getSpeakers } = await import('@/utils/getSpeakers');
66+
const result = await getSpeakers();
67+
68+
expect(result).toEqual(MOCK_SPEAKERS);
69+
expect(warnSpy).toHaveBeenCalledWith(
70+
expect.stringContaining('returning mock data for development'),
71+
);
72+
});
73+
74+
it('returns empty array when token is placeholder in production', async () => {
75+
vi.unstubAllEnvs();
76+
vi.stubEnv('NODE_ENV', 'production');
77+
vi.doMock('@/env', () => mockEnv('<your_token>'));
78+
79+
const { getSpeakers } = await import('@/utils/getSpeakers');
80+
const result = await getSpeakers();
81+
82+
expect(result).toEqual([]);
83+
});
84+
85+
it('returns mock speakers on HTML (Cloudflare) response in development', async () => {
86+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
87+
global.fetch = vi.fn(() =>
88+
Promise.resolve(
89+
createFetchResponse({
90+
ok: false,
91+
status: 403,
92+
statusText: 'Forbidden',
93+
text: () => Promise.resolve('<!DOCTYPE html><html>...</html>'),
94+
}),
95+
),
96+
);
97+
98+
vi.doMock('@/env', () => mockEnv('real-token'));
99+
100+
const { getSpeakers } = await import('@/utils/getSpeakers');
101+
const result = await getSpeakers();
102+
103+
expect(result).toEqual(MOCK_SPEAKERS);
104+
expect(warnSpy).toHaveBeenCalledWith(
105+
expect.stringContaining('Cloudflare/WAF'),
106+
);
107+
});
108+
109+
it('returns empty array on API error in production', async () => {
110+
vi.unstubAllEnvs();
111+
vi.stubEnv('NODE_ENV', 'production');
112+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
113+
global.fetch = vi.fn(() =>
114+
Promise.resolve(
115+
createFetchResponse({
116+
ok: false,
117+
status: 500,
118+
statusText: 'Internal Server Error',
119+
text: () => Promise.resolve('server error'),
120+
}),
121+
),
122+
);
123+
124+
vi.doMock('@/env', () => mockEnv('real-token'));
125+
126+
const { getSpeakers } = await import('@/utils/getSpeakers');
127+
const result = await getSpeakers();
128+
129+
expect(result).toEqual([]);
130+
expect(errorSpy).toHaveBeenCalled();
131+
});
132+
});

src/utils/getSpeakers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { env } from '@/env';
22
import { SpeakersSchema, SpeakerType } from '@/types/speaker';
3+
import { isDevelopment } from '@/utils/isDevelopment';
34
import { MOCK_SPEAKERS } from '@/utils/speakersMock';
45

56
const isPlaceholderToken = (token: string | undefined): boolean => {
@@ -9,8 +10,6 @@ const isPlaceholderToken = (token: string | undefined): boolean => {
910
);
1011
};
1112

12-
const isDevelopment = (): boolean => process.env.NODE_ENV !== 'production';
13-
1413
export const getSpeakers = async (): Promise<SpeakerType[]> => {
1514
if (!env.SPEAKERS_API_URL || isPlaceholderToken(env.SPEAKERS_API_TOKEN)) {
1615
if (isDevelopment()) {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { MOCK_UPCOMING_EVENTS } from '@/utils/eventsMock';
3+
import type { EventType } from '@/types/event';
4+
5+
const createFetchResponse = (overrides: Partial<Response> = {}): Response =>
6+
({
7+
ok: true,
8+
status: 200,
9+
statusText: 'OK',
10+
json: () => Promise.resolve({}),
11+
text: () => Promise.resolve(''),
12+
...overrides,
13+
}) as Response;
14+
15+
describe('getUpcomingEvents', () => {
16+
beforeEach(() => {
17+
vi.resetModules();
18+
vi.stubEnv('NODE_ENV', 'development');
19+
});
20+
21+
afterEach(() => {
22+
vi.unstubAllEnvs();
23+
vi.restoreAllMocks();
24+
});
25+
26+
it('returns parsed API data on success', async () => {
27+
const apiEvent: EventType = {
28+
id: 1,
29+
date_add: 0,
30+
date: '01.01.2030',
31+
time: '18:00',
32+
name: 'Real Meetup',
33+
type: 'Meetup',
34+
url: 'https://example.com',
35+
rsvp: 'https://example.com',
36+
city: 'Wrocław',
37+
address: null,
38+
image: '',
39+
serie: 'real.js',
40+
topic: ['JavaScript'],
41+
};
42+
43+
global.fetch = vi.fn(() =>
44+
Promise.resolve(
45+
createFetchResponse({
46+
json: () => Promise.resolve({ event1: apiEvent }),
47+
}),
48+
),
49+
);
50+
51+
vi.doMock('@/env', () => ({
52+
env: {
53+
EVENTS_API_URL: 'http://localhost:3000/api/events',
54+
},
55+
}));
56+
57+
const { getUpcomingEvents } = await import('@/utils/getUpcomingEvents');
58+
const result = await getUpcomingEvents();
59+
60+
expect(result).toHaveLength(1);
61+
expect(result?.[0]?.name).toBe('Real Meetup');
62+
});
63+
64+
it('returns mock events when API fails in development', async () => {
65+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
66+
global.fetch = vi.fn(() =>
67+
Promise.resolve(
68+
createFetchResponse({
69+
ok: false,
70+
status: 500,
71+
statusText: 'Internal Server Error',
72+
text: () => Promise.resolve('server error'),
73+
}),
74+
),
75+
);
76+
77+
vi.doMock('@/env', () => ({
78+
env: {
79+
EVENTS_API_URL: 'http://localhost:3000/api/events',
80+
},
81+
}));
82+
83+
const { getUpcomingEvents } = await import('@/utils/getUpcomingEvents');
84+
const result = await getUpcomingEvents();
85+
86+
expect(result).toHaveLength(MOCK_UPCOMING_EVENTS.length);
87+
expect(warnSpy).toHaveBeenCalledWith(
88+
expect.stringContaining('using mock events for development'),
89+
);
90+
});
91+
92+
it('returns null when API fails in production', async () => {
93+
vi.unstubAllEnvs();
94+
vi.stubEnv('NODE_ENV', 'production');
95+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
96+
global.fetch = vi.fn(() =>
97+
Promise.resolve(
98+
createFetchResponse({
99+
ok: false,
100+
status: 500,
101+
statusText: 'Internal Server Error',
102+
text: () => Promise.resolve('server error'),
103+
}),
104+
),
105+
);
106+
107+
vi.doMock('@/env', () => ({
108+
env: {
109+
EVENTS_API_URL: 'http://localhost:3000/api/events',
110+
},
111+
}));
112+
113+
const { getUpcomingEvents } = await import('@/utils/getUpcomingEvents');
114+
const result = await getUpcomingEvents();
115+
116+
expect(result).toBeNull();
117+
expect(errorSpy).toHaveBeenCalledWith(
118+
expect.stringContaining('Error fetching upcoming events'),
119+
expect.any(Error),
120+
);
121+
});
122+
});

src/utils/getUpcomingEvents.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { env } from '@/env';
22
import { EventsSchema } from '@/types/event';
33
import { changeCityName } from '@/utils/changeCityName';
44
import { MOCK_UPCOMING_EVENTS } from '@/utils/eventsMock';
5-
6-
const isDevelopment = () => process.env.NODE_ENV !== 'production';
5+
import { isDevelopment } from '@/utils/isDevelopment';
76

87
export const getUpcomingEvents = async () => {
98
try {

src/utils/isDevelopment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const isDevelopment = (): boolean =>
2+
process.env.NODE_ENV !== 'production';

src/utils/speakersMock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const MOCK_SPEAKERS: SpeakerType[] = [
2222
{
2323
id: 3,
2424
name: 'Dev',
25-
surname: null,
25+
surname: '',
2626
slug: 'dev-placeholder',
2727
events_count: 0,
2828
url: 'https://meetjs.pl',

0 commit comments

Comments
 (0)