Skip to content

Commit 567eb21

Browse files
committed
test(redirect): add comprehensive unit tests for RedirectService functionality
1 parent bb8edc1 commit 567eb21

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { RedirectService } from '../RedirectService'
2+
import { UrlRepositoryInterface } from '../../../../repositories/interfaces/UrlRepositoryInterface'
3+
import { CrawlerCheckService } from '../CrawlerCheckService'
4+
import { CookieArrayReducerService } from '../CookieArrayReducerService'
5+
import { LinkStatisticsService } from '../../../analytics/interfaces'
6+
import { RedirectType } from '../..'
7+
8+
const ogUrl = 'https://go.gov.sg'
9+
10+
// Mock the config module
11+
jest.mock('../../../../config', () => ({
12+
logger: {
13+
warn: jest.fn(),
14+
},
15+
ogUrl,
16+
}))
17+
18+
// Mock dependencies
19+
const mockUrlRepository = {
20+
getLongUrl: jest.fn(),
21+
}
22+
23+
const mockCrawlerCheckService = {
24+
isCrawler: jest.fn(),
25+
}
26+
27+
const mockCookieArrayReducerService = {
28+
userHasVisitedShortlink: jest.fn(),
29+
writeShortlinkToCookie: jest.fn(),
30+
}
31+
32+
const mockLinkStatisticsService = {
33+
updateLinkStatistics: jest.fn(),
34+
}
35+
36+
describe('RedirectService', () => {
37+
let redirectService: RedirectService
38+
39+
beforeEach(() => {
40+
// Reset mocks
41+
jest.clearAllMocks()
42+
43+
// Setup default mock returns
44+
mockUrlRepository.getLongUrl.mockResolvedValue('https://example.com')
45+
mockCrawlerCheckService.isCrawler.mockReturnValue(false)
46+
mockCookieArrayReducerService.userHasVisitedShortlink.mockReturnValue(false)
47+
mockCookieArrayReducerService.writeShortlinkToCookie.mockReturnValue([
48+
'test',
49+
])
50+
51+
// Create service instance with mocked dependencies
52+
redirectService = new RedirectService(
53+
mockUrlRepository as unknown as UrlRepositoryInterface,
54+
mockCrawlerCheckService as unknown as CrawlerCheckService,
55+
mockCookieArrayReducerService as unknown as CookieArrayReducerService,
56+
mockLinkStatisticsService as unknown as LinkStatisticsService,
57+
)
58+
})
59+
60+
describe('referrer validation', () => {
61+
it('should show transition page for exact ogUrl match when user has not visited before', async () => {
62+
const result = await redirectService.redirectFor(
63+
'test',
64+
undefined,
65+
'Mozilla/5.0',
66+
ogUrl,
67+
)
68+
69+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
70+
})
71+
72+
it('should show transition page for ogUrl with path when user has not visited before', async () => {
73+
const result = await redirectService.redirectFor(
74+
'test',
75+
undefined,
76+
'Mozilla/5.0',
77+
`${ogUrl}/some-path`,
78+
)
79+
80+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
81+
})
82+
83+
it('should show transition page for ogUrl with query params when user has not visited before', async () => {
84+
const result = await redirectService.redirectFor(
85+
'test',
86+
undefined,
87+
'Mozilla/5.0',
88+
`${ogUrl}?param=value`,
89+
)
90+
91+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
92+
})
93+
94+
it('should show transition page for ogUrl with different protocol when user has not visited before', async () => {
95+
const result = await redirectService.redirectFor(
96+
'test',
97+
undefined,
98+
'Mozilla/5.0',
99+
'http://go.gov.sg', // different protocol
100+
)
101+
102+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
103+
})
104+
105+
it('should NOT allow transition page bypass for malicious domain that starts with ogUrl', async () => {
106+
const result = await redirectService.redirectFor(
107+
'test',
108+
undefined,
109+
'Mozilla/5.0',
110+
`${ogUrl}.malicious.com`,
111+
)
112+
113+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
114+
})
115+
116+
it('should NOT allow transition page bypass for subdomain of ogUrl', async () => {
117+
const result = await redirectService.redirectFor(
118+
'test',
119+
undefined,
120+
'Mozilla/5.0',
121+
`staging.${ogUrl}`,
122+
)
123+
124+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
125+
})
126+
127+
it('should NOT allow transition page bypass for domain containing ogUrl', async () => {
128+
const result = await redirectService.redirectFor(
129+
'test',
130+
undefined,
131+
'Mozilla/5.0',
132+
`https://staging.${ogUrl}.malicious.com`,
133+
)
134+
135+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
136+
})
137+
138+
it('should NOT allow transition page bypass for completely different domain', async () => {
139+
const result = await redirectService.redirectFor(
140+
'test',
141+
undefined,
142+
'Mozilla/5.0',
143+
'https://malicious.com',
144+
)
145+
146+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
147+
})
148+
149+
it('should NOT allow transition page bypass for invalid referrer URL', async () => {
150+
const result = await redirectService.redirectFor(
151+
'test',
152+
undefined,
153+
'Mozilla/5.0',
154+
'not-a-valid-url',
155+
)
156+
157+
expect(result.redirectType).toBe(RedirectType.TransitionPage)
158+
})
159+
160+
// TODO: not a regression, but to consider if we want to fix this
161+
it('should allow direct redirect when user has visited the shortlink before, even from malicious site', async () => {
162+
// Mock that user has visited this shortlink before
163+
mockCookieArrayReducerService.userHasVisitedShortlink.mockReturnValue(
164+
true,
165+
)
166+
167+
const result = await redirectService.redirectFor(
168+
'test',
169+
['test'], // past visits include this shortlink
170+
'Mozilla/5.0',
171+
'https://malicious.com', // even from malicious site
172+
)
173+
174+
expect(result.redirectType).toBe(RedirectType.Direct)
175+
})
176+
177+
it('should allow direct redirect when user has visited the shortlink before, even from trusted page', async () => {
178+
// Mock that user has visited this shortlink before
179+
mockCookieArrayReducerService.userHasVisitedShortlink.mockReturnValue(
180+
true,
181+
)
182+
183+
const result = await redirectService.redirectFor(
184+
'test',
185+
['test'], // past visits include this shortlink
186+
'Mozilla/5.0',
187+
ogUrl, // from trusted page
188+
)
189+
190+
expect(result.redirectType).toBe(RedirectType.Direct)
191+
})
192+
})
193+
})

0 commit comments

Comments
 (0)