Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1import { describe, it, expect } from 'bun:test'
2import { parseRedirectsFile, matchRedirectRule } from './redirects';
3
4describe('parseRedirectsFile', () => {
5 it('should parse simple redirects', () => {
6 const content = `
7# Comment line
8/old-path /new-path
9/home / 301
10`;
11 const rules = parseRedirectsFile(content);
12 expect(rules).toHaveLength(2);
13 expect(rules[0]).toMatchObject({
14 from: '/old-path',
15 to: '/new-path',
16 status: 301,
17 force: false,
18 });
19 expect(rules[1]).toMatchObject({
20 from: '/home',
21 to: '/',
22 status: 301,
23 force: false,
24 });
25 });
26
27 it('should parse redirects with different status codes', () => {
28 const content = `
29/temp-redirect /target 302
30/rewrite /content 200
31/not-found /404 404
32`;
33 const rules = parseRedirectsFile(content);
34 expect(rules).toHaveLength(3);
35 expect(rules[0]?.status).toBe(302);
36 expect(rules[1]?.status).toBe(200);
37 expect(rules[2]?.status).toBe(404);
38 });
39
40 it('should parse force redirects', () => {
41 const content = `/force-path /target 301!`;
42 const rules = parseRedirectsFile(content);
43 expect(rules[0]?.force).toBe(true);
44 expect(rules[0]?.status).toBe(301);
45 });
46
47 it('should parse splat redirects', () => {
48 const content = `/news/* /blog/:splat`;
49 const rules = parseRedirectsFile(content);
50 expect(rules[0]?.from).toBe('/news/*');
51 expect(rules[0]?.to).toBe('/blog/:splat');
52 });
53
54 it('should parse placeholder redirects', () => {
55 const content = `/blog/:year/:month/:day /posts/:year-:month-:day`;
56 const rules = parseRedirectsFile(content);
57 expect(rules[0]?.from).toBe('/blog/:year/:month/:day');
58 expect(rules[0]?.to).toBe('/posts/:year-:month-:day');
59 });
60
61 it('should parse country-based redirects', () => {
62 const content = `/ /anz 302 Country=au,nz`;
63 const rules = parseRedirectsFile(content);
64 expect(rules[0]?.conditions?.country).toEqual(['au', 'nz']);
65 });
66
67 it('should parse language-based redirects', () => {
68 const content = `/products /en/products 301 Language=en`;
69 const rules = parseRedirectsFile(content);
70 expect(rules[0]?.conditions?.language).toEqual(['en']);
71 });
72
73 it('should parse cookie-based redirects', () => {
74 const content = `/* /legacy/:splat 200 Cookie=is_legacy,my_cookie`;
75 const rules = parseRedirectsFile(content);
76 expect(rules[0]?.conditions?.cookie).toEqual(['is_legacy', 'my_cookie']);
77 });
78});
79
80describe('matchRedirectRule', () => {
81 it('should match exact paths', () => {
82 const rules = parseRedirectsFile('/old-path /new-path');
83 const match = matchRedirectRule('/old-path', rules);
84 expect(match).toBeTruthy();
85 expect(match?.targetPath).toBe('/new-path');
86 expect(match?.status).toBe(301);
87 });
88
89 it('should match paths with trailing slash', () => {
90 const rules = parseRedirectsFile('/old-path /new-path');
91 const match = matchRedirectRule('/old-path/', rules);
92 expect(match).toBeTruthy();
93 expect(match?.targetPath).toBe('/new-path');
94 });
95
96 it('should match splat patterns', () => {
97 const rules = parseRedirectsFile('/news/* /blog/:splat');
98 const match = matchRedirectRule('/news/2024/01/15/my-post', rules);
99 expect(match).toBeTruthy();
100 expect(match?.targetPath).toBe('/blog/2024/01/15/my-post');
101 });
102
103 it('should match placeholder patterns', () => {
104 const rules = parseRedirectsFile('/blog/:year/:month/:day /posts/:year-:month-:day');
105 const match = matchRedirectRule('/blog/2024/01/15', rules);
106 expect(match).toBeTruthy();
107 expect(match?.targetPath).toBe('/posts/2024-01-15');
108 });
109
110 it('should preserve query strings for 301/302 redirects', () => {
111 const rules = parseRedirectsFile('/old /new 301');
112 const match = matchRedirectRule('/old', rules, {
113 queryParams: { foo: 'bar', baz: 'qux' },
114 });
115 expect(match?.targetPath).toContain('?');
116 expect(match?.targetPath).toContain('foo=bar');
117 expect(match?.targetPath).toContain('baz=qux');
118 });
119
120 it('should match based on query parameters', () => {
121 const rules = parseRedirectsFile('/store id=:id /blog/:id 301');
122 const match = matchRedirectRule('/store', rules, {
123 queryParams: { id: 'my-post' },
124 });
125 expect(match).toBeTruthy();
126 expect(match?.targetPath).toContain('/blog/my-post');
127 });
128
129 it('should not match when query params are missing', () => {
130 const rules = parseRedirectsFile('/store id=:id /blog/:id 301');
131 const match = matchRedirectRule('/store', rules, {
132 queryParams: {},
133 });
134 expect(match).toBeNull();
135 });
136
137 it('should match based on country header', () => {
138 const rules = parseRedirectsFile('/ /aus 302 Country=au');
139 const match = matchRedirectRule('/', rules, {
140 headers: { 'cf-ipcountry': 'AU' },
141 });
142 expect(match).toBeTruthy();
143 expect(match?.targetPath).toBe('/aus');
144 });
145
146 it('should not match wrong country', () => {
147 const rules = parseRedirectsFile('/ /aus 302 Country=au');
148 const match = matchRedirectRule('/', rules, {
149 headers: { 'cf-ipcountry': 'US' },
150 });
151 expect(match).toBeNull();
152 });
153
154 it('should match based on language header', () => {
155 const rules = parseRedirectsFile('/products /en/products 301 Language=en');
156 const match = matchRedirectRule('/products', rules, {
157 headers: { 'accept-language': 'en-US,en;q=0.9' },
158 });
159 expect(match).toBeTruthy();
160 expect(match?.targetPath).toBe('/en/products');
161 });
162
163 it('should match based on cookie presence', () => {
164 const rules = parseRedirectsFile('/* /legacy/:splat 200 Cookie=is_legacy');
165 const match = matchRedirectRule('/some-path', rules, {
166 cookies: { is_legacy: 'true' },
167 });
168 expect(match).toBeTruthy();
169 expect(match?.targetPath).toBe('/legacy/some-path');
170 });
171
172 it('should return first matching rule', () => {
173 const content = `
174/path /first
175/path /second
176`;
177 const rules = parseRedirectsFile(content);
178 const match = matchRedirectRule('/path', rules);
179 expect(match?.targetPath).toBe('/first');
180 });
181
182 it('should match more specific rules before general ones', () => {
183 const content = `
184/jobs/customer-ninja /careers/support
185/jobs/* /careers/:splat
186`;
187 const rules = parseRedirectsFile(content);
188
189 const match1 = matchRedirectRule('/jobs/customer-ninja', rules);
190 expect(match1?.targetPath).toBe('/careers/support');
191
192 const match2 = matchRedirectRule('/jobs/developer', rules);
193 expect(match2?.targetPath).toBe('/careers/developer');
194 });
195
196 it('should handle SPA routing pattern', () => {
197 const rules = parseRedirectsFile('/* /index.html 200');
198
199 // Should match any path
200 const match1 = matchRedirectRule('/about', rules);
201 expect(match1).toBeTruthy();
202 expect(match1?.targetPath).toBe('/index.html');
203 expect(match1?.status).toBe(200);
204
205 const match2 = matchRedirectRule('/users/123/profile', rules);
206 expect(match2).toBeTruthy();
207 expect(match2?.targetPath).toBe('/index.html');
208 expect(match2?.status).toBe(200);
209
210 const match3 = matchRedirectRule('/', rules);
211 expect(match3).toBeTruthy();
212 expect(match3?.targetPath).toBe('/index.html');
213 });
214});
215