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