Skip to content

Commit 39dce78

Browse files
authored
chore: enable if-none-match conformance tests (#944)
Enables tests for if-none-match headers
1 parent 8128b6e commit 39dce78

File tree

7 files changed

+183
-50
lines changed

7 files changed

+183
-50
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@helia/http": "^3.0.8",
6363
"@helia/interface": "^6.0.2",
6464
"@helia/routers": "^4.0.3",
65-
"@helia/verified-fetch": "^5.0.4",
65+
"@helia/verified-fetch": "^5.1.0",
6666
"@ipld/dag-cbor": "^9.2.5",
6767
"@ipld/dag-json": "^10.2.5",
6868
"@ipld/dag-pb": "^4.1.5",

src/sw/handlers/content-request-handler.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interface FetchHandlerArg {
4949
const ONE_HOUR_IN_SECONDS = 3600
5050

5151
function getCacheKey (url: URL, headers: Headers, renderPreview: boolean, config: ConfigDb): string {
52-
return `${url}-${headers.get('accept') ?? ''}-preview-${renderPreview}-indexes-${config.supportDirectoryIndexes}-redirects-${config.supportWebRedirects}`
52+
return `${url}-${headers.get('accept')}-preview-${renderPreview}-indexes-${config.supportDirectoryIndexes}-redirects-${config.supportWebRedirects}-match-${headers.get('if-none-match')}`
5353
}
5454

5555
async function getResponseFromCacheOrFetch (args: FetchHandlerArg): Promise<Response> {
@@ -295,41 +295,41 @@ async function fetchHandler ({ url, headers, renderPreview, event, logs, subdoma
295295
renderPreview = shouldRenderDirectory(url, config, accept)
296296
}
297297

298-
if (!response.ok) {
298+
if (response.status > 399) {
299299
return fetchErrorPageResponse(resource, init, response, await response.text(), providers, config, firstInstallTime, logs)
300300
}
301301

302-
if (renderPreview) {
303-
try {
304-
return renderEntityPageResponse(url, headers, response, await response.arrayBuffer())
305-
} catch (err: any) {
306-
log.error('error while loading body to render - %e', err)
307-
308-
// if the response content involves loading more than one block and
309-
// loading a subsequent block fails, the `.arrayBuffer()` promise will
310-
// reject with an opaque 'TypeError: Failed to fetch' so show a 502 Bad
311-
// Gateway with debugging information
312-
return fetchErrorPageResponse(resource, init, new Response('', {
313-
status: 502,
314-
statusText: 'Bad Gateway',
315-
headers: {
316-
'Content-Type': 'application/json'
317-
}
318-
}), JSON.stringify(errorToObject(err), null, 2), providers, config, firstInstallTime, logs)
319-
}
320-
}
321-
322-
if (url.searchParams.get('download') === 'true') {
323-
// override inline attachments if present
324-
let contentDisposition = response.headers.get('content-disposition')
302+
if (response.status > 199 && response.status < 300) {
303+
if (renderPreview) {
304+
try {
305+
return renderEntityPageResponse(url, headers, response, await response.arrayBuffer())
306+
} catch (err: any) {
307+
log.error('error while loading body to render - %e', err)
308+
309+
// if the response content involves loading more than one block and
310+
// loading a subsequent block fails, the `.arrayBuffer()` promise will
311+
// reject with an opaque 'TypeError: Failed to fetch' so show a 502 Bad
312+
// Gateway with debugging information
313+
return fetchErrorPageResponse(resource, init, new Response('', {
314+
status: 502,
315+
statusText: 'Bad Gateway',
316+
headers: {
317+
'Content-Type': 'application/json'
318+
}
319+
}), JSON.stringify(errorToObject(err), null, 2), providers, config, firstInstallTime, logs)
320+
}
321+
} else if (url.searchParams.get('download') === 'true') {
322+
// override inline attachments if present
323+
let contentDisposition = response.headers.get('content-disposition')
324+
325+
if (contentDisposition == null || contentDisposition === '') {
326+
contentDisposition = 'attachment'
327+
} else if (contentDisposition.startsWith('inline')) {
328+
contentDisposition = contentDisposition.replace('inline', 'attachment')
329+
}
325330

326-
if (contentDisposition == null || contentDisposition === '') {
327-
contentDisposition = 'attachment'
328-
} else if (contentDisposition.startsWith('inline')) {
329-
contentDisposition = contentDisposition.replace('inline', 'attachment')
331+
response.headers.set('content-disposition', contentDisposition)
330332
}
331-
332-
response.headers.set('content-disposition', contentDisposition)
333333
}
334334

335335
// Create a completely new response object with the same body, status,

test-conformance/fixtures/expected-passing-tests.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,16 @@
451451
"TestGatewayCache/HEAD_for_%2Fipfs%2F_with_only-if-cached_fails_when_not_in_local_datastore/Status_code",
452452
"TestGatewayCache/GET_for_%2Fipfs%2F_with_only-if-cached_fails_when_not_in_local_datastore",
453453
"TestGatewayCache/GET_for_%2Fipfs%2F_with_only-if-cached_fails_when_not_in_local_datastore/Status_code",
454+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified",
455+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
456+
"TestGatewayCache/GET_for_%2Fipfs%2F_dir_with_index.html_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified",
457+
"TestGatewayCache/GET_for_%2Fipfs%2F_dir_with_index.html_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
458+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_third_Etag_in_If-None-Match_returns_304_Not_Modified",
459+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_third_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
460+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_weak_Etag_in_If-None-Match_returns_304_Not_Modified",
461+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_weak_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
462+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_wildcard_Etag_in_If-None-Match_returns_304_Not_Modified",
463+
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_wildcard_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
454464
"TestGatewayCacheWithIPNS",
455465
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_DAG-JSON_succeeds",
456466
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_DAG-JSON_succeeds/Check_0",
@@ -464,6 +474,8 @@
464474
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_JSON_succeeds/Check_1",
465475
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_JSON_succeeds/Check_1/Check_0",
466476
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_JSON_succeeds/Check_1/Check_1",
477+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified",
478+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
467479
"TestGatewaySymlink",
468480
"TestGatewaySymlink/Test_the_directory_raw_query",
469481
"TestGatewaySymlink/Test_the_directory_raw_query/Status_code",
@@ -498,7 +510,6 @@
498510
"TestPathGatewayMiscellaneous",
499511
"TestPathGatewayMiscellaneous/GET_for_%2Fipfs%2F_file_whose_filename_contains_percentage-encoded_characters_works",
500512
"TestPathGatewayMiscellaneous/GET_for_%2Fipfs%2F_file_whose_filename_contains_percentage-encoded_characters_works/Body",
501-
"TestRedirectsFileWithIfNoneMatchHeader",
502513
"TestUnixFSDirectoryListingOnSubdomainGateway",
503514
"TestGatewaySubdomains",
504515
"TestTrustlessCarPathing",

test-conformance/fixtures/get-wontfix-tests.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,10 @@ export function getWontFixTests (): string[] {
3030
'TestGatewayCache/HEAD_for_%2Fipfs%2F_with_only-if-cached_succeeds_when_in_local_datastore',
3131
'TestGatewayCache/GET_for_%2Fipfs%2F_with_only-if-cached_succeeds_when_in_local_datastore',
3232

33-
// we cannot send if-none-match header
34-
'TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified',
35-
'TestGatewayCache/GET_for_%2Fipfs%2F_dir_with_index.html_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified',
36-
'TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_third_Etag_in_If-None-Match_returns_304_Not_Modified',
37-
'TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_weak_Etag_in_If-None-Match_returns_304_Not_Modified',
38-
'TestGatewayCache/GET_for_%2Fipfs%2F_file_with_wildcard_Etag_in_If-None-Match_returns_304_Not_Modified',
33+
// this test sends an empty weak etag (W/"") which should disable etag
34+
// matching but instead it expects it to match and return a 304?
35+
// https://github.com/ipfs/gateway-conformance/issues/261
3936
'TestGatewayCache/GET_for_%2Fipfs%2F_dir_listing_with_matching_weak_Etag_in_If-None-Match_returns_304_Not_Modified',
40-
'TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_file_with_matching_Etag_in_If-None-Match_returns_304_Not_Modified',
41-
'TestRedirectsFileWithIfNoneMatchHeader/request_for_%2F%2F%7Bdnslink%7D%2Fmissing-page_with_If-None-Match_returns_304',
4237

4338
// redirects file isn't supported yet
4439
'TestRedirectsFileSupport',

test-e2e/fixtures/load-ipns-records.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import { glob } from 'glob'
66
import itAll from 'it-all'
77
import type { KuboNode } from 'ipfsd-ctl'
88

9+
const FIXTURE_DIRS = [
10+
`${process.cwd()}/test-e2e/fixtures/data/**/*.ipns-record`,
11+
`${process.cwd()}/test-conformance/fixtures/**/*.ipns-record`
12+
]
13+
914
export async function loadIpnsRecords (node: KuboNode): Promise<void> {
1015
console.info('Loading ipns records')
1116
let loadedIpnsRecords = 0
1217

13-
for (const ipnsRecord of await glob([`${process.cwd()}/test-e2e/fixtures/data/**/*.ipns-record`])) {
18+
for (const ipnsRecord of await glob(FIXTURE_DIRS)) {
1419
loadedIpnsRecords++
1520
console.info('Loading *.ipns-record fixture fullpath:', ipnsRecord)
1621

test-e2e/fixtures/load-kubo-fixtures.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,31 @@ const log = logger('kubo-init')
2727

2828
// This needs to match the `repo` property provided to `ipfsd-ctl` in `createKuboNode` so our kubo instance in tests use the same repo
2929
export const kuboRepoDir = `${tmpdir()}/.ipfs/${Date.now()}`
30-
export const GWC_FIXTURES_PATH = resolve(__dirname, 'data/gateway-conformance-fixtures')
30+
export const GWC_FIXTURES_PATH = resolve(__dirname, '../../test-conformance/fixtures/gateway-conformance-fixtures')
3131

3232
export async function downloadFixtures (force = false): Promise<void> {
3333
if (!force) {
34-
// if the fixtures are already downloaded, we don't need to download them again
35-
const allFixtures = await glob([`${GWC_FIXTURES_PATH}/**/*.car`, `${GWC_FIXTURES_PATH}/**/*.ipns-record`, `${GWC_FIXTURES_PATH}/dnslinks.json`])
36-
if (allFixtures.length > 0) {
37-
log('Fixtures already downloaded')
34+
// skip downloading fixtures if they are already present
35+
const allFixtures = await Promise.all([
36+
glob([`${GWC_FIXTURES_PATH}/**/*.car`]),
37+
glob([`${GWC_FIXTURES_PATH}/**/*.ipns-record`]),
38+
glob([`${GWC_FIXTURES_PATH}/dnslinks.json`])
39+
])
40+
41+
if (allFixtures.every(fixtures => fixtures.length > 0)) {
42+
console.info('Fixtures already downloaded')
3843
return
3944
}
4045
}
4146

42-
log('Downloading fixtures')
47+
console.info('Downloading fixtures')
4348
try {
4449
await $`docker run -v ${process.cwd()}:/workspace -w /workspace ghcr.io/ipfs/gateway-conformance:v0.7.1 extract-fixtures --directory ${relative('.', GWC_FIXTURES_PATH)} --merged false`
45-
} catch (err) {
50+
} catch (err: any) {
51+
if (err.message.includes('docker')) {
52+
throw err
53+
}
54+
4655
log.error('error downloading fixtures, assuming current or previous success - %e', err)
4756
}
4857
}
@@ -56,11 +65,16 @@ export async function getIpfsNsMap (): Promise<string> {
5665
return ipfsNsMap
5766
}
5867

68+
const FIXTURE_DIRS = [
69+
`${process.cwd()}/test-e2e/fixtures/data/**/*.car`,
70+
`${process.cwd()}/test-conformance/fixtures/**/*.car`
71+
]
72+
5973
export async function loadCarFixtures (node: KuboNode): Promise<void> {
6074
// const execaOptions = getExecaOptions()
6175
let loadedCarFiles = 0
6276

63-
for (const carFile of await glob([`${process.cwd()}/test-e2e/fixtures/data/**/*.car`])) {
77+
for (const carFile of await glob(FIXTURE_DIRS)) {
6478
loadedCarFiles++
6579
console.info('Loading *.car fixture %s', carFile)
6680

test-e2e/if-none-match.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { stop } from '@libp2p/interface'
2+
import * as cbor from 'cborg'
3+
import { createKuboRPCClient } from 'kubo-rpc-client'
4+
import { testPathRouting as test, expect } from './fixtures/config-test-fixtures.js'
5+
import { makeFetchRequest } from './fixtures/make-range-request.ts'
6+
import { setConfig } from './fixtures/set-sw-config.ts'
7+
import { waitForServiceWorker } from './fixtures/wait-for-service-worker.ts'
8+
import type { KuboRPCClient } from 'kubo-rpc-client'
9+
import type { CID } from 'multiformats/cid'
10+
11+
const object = {
12+
hello: 'world'
13+
}
14+
15+
test.describe('if-none-match', () => {
16+
let kubo: KuboRPCClient
17+
let cid: CID
18+
let block: Uint8Array
19+
20+
test.beforeEach(async ({ page }) => {
21+
if (process.env.KUBO_GATEWAY == null || process.env.KUBO_GATEWAY === '') {
22+
throw new Error('KUBO_GATEWAY not set')
23+
}
24+
25+
kubo = createKuboRPCClient(process.env.KUBO_RPC)
26+
27+
block = cbor.encode(object)
28+
cid = await kubo.block.put(block, {
29+
format: 'cbor'
30+
})
31+
32+
await waitForServiceWorker(page)
33+
await setConfig(page, {
34+
gateways: [
35+
process.env.KUBO_GATEWAY
36+
],
37+
routers: [
38+
process.env.KUBO_GATEWAY
39+
],
40+
acceptOriginIsolationWarning: true,
41+
renderHTMLViews: false
42+
})
43+
})
44+
45+
test.afterEach(async () => {
46+
await stop(kubo)
47+
})
48+
49+
test('should return 304 if the etag matches', async ({ page }) => {
50+
const response0 = await makeFetchRequest(page, `/ipfs/${cid}`)
51+
expect(response0.status()).toBe(200)
52+
53+
const etag = await response0.headerValue('etag') ?? ''
54+
expect(etag).toContain(cid.toString())
55+
56+
const response1 = await makeFetchRequest(page, `/ipfs/${cid}`, {
57+
headers: {
58+
'if-none-match': etag
59+
}
60+
})
61+
expect(response1.status()).toBe(304)
62+
})
63+
64+
test('should return 304 if the etag matches one of the etags', async ({ page }) => {
65+
const response0 = await makeFetchRequest(page, `/ipfs/${cid}`)
66+
expect(response0.status()).toBe(200)
67+
68+
const etag = await response0.headerValue('etag') ?? ''
69+
expect(etag).toContain(cid.toString())
70+
71+
const response1 = await makeFetchRequest(page, `/ipfs/${cid}`, {
72+
headers: {
73+
'if-none-match': `"foo", W/"bar", ${etag}, "baz"`
74+
}
75+
})
76+
expect(response1.status()).toBe(304)
77+
})
78+
79+
test('should return 304 if the etag matches a wildcard', async ({ page }) => {
80+
const response0 = await makeFetchRequest(page, `/ipfs/${cid}`)
81+
expect(response0.status()).toBe(200)
82+
83+
const etag = await response0.headerValue('etag') ?? ''
84+
expect(etag).toContain(cid.toString())
85+
86+
const response1 = await makeFetchRequest(page, `/ipfs/${cid}`, {
87+
headers: {
88+
'if-none-match': '*'
89+
}
90+
})
91+
expect(response1.status()).toBe(304)
92+
})
93+
94+
test('should return 304 if the etag matches a weak value', async ({ page }) => {
95+
const response0 = await makeFetchRequest(page, `/ipfs/${cid}`)
96+
expect(response0.status()).toBe(200)
97+
98+
const etag = await response0.headerValue('etag') ?? ''
99+
expect(etag).toContain(cid.toString())
100+
101+
const response1 = await makeFetchRequest(page, `/ipfs/${cid}`, {
102+
headers: {
103+
'if-none-match': `W/"${cid}.dag-cbor"`
104+
}
105+
})
106+
expect(response1.status()).toBe(304)
107+
})
108+
})

0 commit comments

Comments
 (0)