Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix folders ending with .extension being treated as files then 404ing

+133 -18
+117
apps/hosting-service/src/lib/file-serving.integration.test.ts
··· 1 + import { beforeEach, describe, expect, mock, test } from 'bun:test' 2 + 3 + // Fake storage shared across tests; reset in beforeEach 4 + type FakeEntry = { data: Uint8Array; mimeType?: string; encoding?: string; checksum?: string } 5 + const storageData = new Map<string, FakeEntry>() 6 + 7 + const fakeStorage = { 8 + async get(key: string) { 9 + const entry = storageData.get(key) 10 + return entry?.data ?? null 11 + }, 12 + async getWithMetadata(key: string) { 13 + const entry = storageData.get(key) 14 + if (!entry) return null 15 + return { 16 + data: entry.data, 17 + metadata: { 18 + key, 19 + size: entry.data.length, 20 + createdAt: new Date(), 21 + lastAccessed: new Date(), 22 + accessCount: 0, 23 + checksum: entry.checksum ?? 'test-checksum', 24 + customMetadata: { mimeType: entry.mimeType, encoding: entry.encoding }, 25 + }, 26 + source: 'cold' as const, 27 + } 28 + }, 29 + async *listKeys(_prefix?: string): AsyncGenerator<string> {}, 30 + } 31 + 32 + const noopTier = { 33 + async get() { 34 + return null 35 + }, 36 + async getWithMetadata() { 37 + return null 38 + }, 39 + async set() {}, 40 + async delete() {}, 41 + async deleteMany() {}, 42 + async exists() { 43 + return false 44 + }, 45 + async *listKeys() {}, 46 + async getMetadata() { 47 + return null 48 + }, 49 + async setMetadata() {}, 50 + async getStats() { 51 + return { entries: 0, sizeBytes: 0 } 52 + }, 53 + async clear() {}, 54 + } 55 + 56 + mock.module('./storage', () => ({ 57 + storage: fakeStorage, 58 + hotTier: noopTier, 59 + warmTier: undefined, 60 + getStorageConfig: () => ({}), 61 + })) 62 + mock.module('./db', () => ({ 63 + getSiteCache: async () => null, 64 + getSiteSettingsCache: async () => null, 65 + CACHE_ONLY: true, 66 + })) 67 + mock.module('./utils', () => ({ 68 + getCachedSettings: async () => null, 69 + })) 70 + mock.module('./on-demand-cache', () => ({ 71 + fetchAndCacheSite: async () => false, 72 + })) 73 + 74 + const { serveFileInternal } = await import('./file-serving') 75 + 76 + const DID = 'did:plc:test' 77 + const RKEY = 'hydrant-docs' 78 + 79 + function storeFile(path: string, body: string, mimeType = 'text/html') { 80 + storageData.set(`${DID}/${RKEY}/${path}`, { 81 + data: new TextEncoder().encode(body), 82 + mimeType, 83 + }) 84 + } 85 + 86 + describe('serveFileInternal directory-index fallback for extensioned paths', () => { 87 + beforeEach(() => { 88 + storageData.clear() 89 + }) 90 + 91 + test('serves index.html when requested path with .md extension is actually a directory', async () => { 92 + // Astro emits concepts/relay.md/ as a directory containing index.html 93 + storeFile('concepts/relay.md/index.html', '<html>relay docs</html>') 94 + 95 + const response = await serveFileInternal(DID, RKEY, 'concepts/relay.md') 96 + 97 + expect(response.status).toBe(200) 98 + expect(response.headers.get('Content-Type')).toContain('text/html') 99 + expect(await response.text()).toBe('<html>relay docs</html>') 100 + }) 101 + 102 + test('still serves a direct file when both file and directory-style entry would match', async () => { 103 + // If the file exists directly, it wins over the directory-index fallback 104 + storeFile('concepts/relay.md', 'raw markdown', 'text/markdown') 105 + 106 + const response = await serveFileInternal(DID, RKEY, 'concepts/relay.md') 107 + 108 + expect(response.status).toBe(200) 109 + expect(await response.text()).toBe('raw markdown') 110 + }) 111 + 112 + test('returns 404 when neither the file nor an index under it exists', async () => { 113 + const response = await serveFileInternal(DID, RKEY, 'concepts/missing.md') 114 + 115 + expect(response.status).toBe(404) 116 + }) 117 + })
+16 -18
apps/hosting-service/src/lib/file-serving.ts
··· 520 520 } 521 521 await markExpectedMiss(fileRequestPath) 522 522 523 - // Try index files for directory-like paths 524 - if (!hasFileExtension(fileRequestPath)) { 525 - for (const indexFileName of indexFiles) { 526 - const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName 527 - const indexResult = await span(trace, `storage:${indexPath}`, () => getFileWithMetadata(did, rkey, indexPath)) 528 - if (indexResult) { 529 - return buildResponseFromStorageResult(indexResult, indexPath, settings, requestHeaders) 530 - } 531 - await markExpectedMiss(indexPath) 523 + // Try index files for directory-like paths (even with extensions, 524 + // e.g. Astro emits `relay.md/index.html` for .md routes) 525 + for (const indexFileName of indexFiles) { 526 + const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName 527 + const indexResult = await span(trace, `storage:${indexPath}`, () => getFileWithMetadata(did, rkey, indexPath)) 528 + if (indexResult) { 529 + return buildResponseFromStorageResult(indexResult, indexPath, settings, requestHeaders) 532 530 } 531 + await markExpectedMiss(indexPath) 533 532 } 534 533 535 534 // Try clean URLs: /about -> /about.html ··· 869 868 } 870 869 await markExpectedMiss(fileRequestPath) 871 870 872 - // Try index files for directory-like paths 873 - if (!hasFileExtension(fileRequestPath)) { 874 - for (const indexFileName of indexFiles) { 875 - const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName 876 - const indexResult = await span(trace, `storage:${indexPath}`, () => getFileForRequest(did, rkey, indexPath, true)) 877 - if (indexResult) { 878 - return await buildResponse(indexResult) 879 - } 880 - await markExpectedMiss(indexPath) 871 + // Try index files for directory-like paths (even with extensions, 872 + // e.g. Astro emits `relay.md/index.html` for .md routes) 873 + for (const indexFileName of indexFiles) { 874 + const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName 875 + const indexResult = await span(trace, `storage:${indexPath}`, () => getFileForRequest(did, rkey, indexPath, true)) 876 + if (indexResult) { 877 + return await buildResponse(indexResult) 881 878 } 879 + await markExpectedMiss(indexPath) 882 880 } 883 881 884 882 // Try clean URLs: /about -> /about.html