import fetch from 'cross-fetch'; import { DataFactory } from 'n3'; import type { App } from '../../src/init/App'; import { deleteResource, expectQuads, getResource, patchResource, putResource } from '../util/FetchUtil'; import { getPort } from '../util/Util'; import { getDefaultVariables, getPresetConfigPath, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder, } from './Config'; const { namedNode, quad } = DataFactory; const port = getPort('Conditions'); const baseUrl = `http://localhost:${port}/`; // File stores handle last-modified dates differently so need to test both const rootFilePath = getTestFolder('conditions'); const stores: [string, any][] = [ [ 'in-memory storage', { storeConfig: 'storage/backend/memory.json', teardown: jest.fn(), }], [ 'on-disk storage', { storeConfig: 'storage/backend/file.json', teardown: async(): Promise => removeFolder(rootFilePath), }], ]; describe.each(stores)('A server supporting conditions with %s', (name, { storeConfig, teardown }): void => { let app: App; beforeAll(async(): Promise => { const variables = { ...getDefaultVariables(port, baseUrl), 'urn:solid-server:default:variable:rootFilePath': rootFilePath, }; // Create and start the server const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', [ getPresetConfigPath(storeConfig), getTestConfigPath('ldp-with-auth.json'), ], variables, ) as Record; ({ app } = instances); await app.start(); }); afterAll(async(): Promise => { await teardown(); await app.stop(); }); it('prevents operations on existing resources with "if-none-match: *" header.', async(): Promise => { const documentUrl = `${baseUrl}document1.txt`; // PUT await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE' }); // Overwrite fails because of header let response = await fetch(documentUrl, { method: 'PUT', headers: { 'content-type': 'text/plain', 'if-none-match': '*' }, body: 'TESTFILE1', }); expect(response.status).toBe(412); // Verify original contents stayed the same response = await getResource(documentUrl); await expect(response.text()).resolves.toBe('TESTFILE'); // DELETE expect(await deleteResource(documentUrl)).toBeUndefined(); }); it('prevents creating new resources with "if-match: *" header.', async(): Promise => { const documentUrl = `${baseUrl}document2.txt`; const query = 'INSERT { } WHERE {}'; // PATCH fails because of header let response = await fetch(documentUrl, { method: 'PATCH', headers: { 'content-type': 'application/sparql-update', 'if-match': '*' }, body: query, }); expect(response.status).toBe(412); // PUT await patchResource(documentUrl, query, 'sparql'); // PUT with header now succeeds const query2 = 'INSERT { } WHERE {}'; response = await fetch(documentUrl, { method: 'PATCH', headers: { 'content-type': 'application/sparql-update', 'if-match': '*' }, body: query2, }); expect(response.status).toBe(205); // Verify the contents got updated response = await getResource(documentUrl); const expected = [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')), quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')), ]; await expectQuads(response, expected, true); // DELETE expect(await deleteResource(documentUrl)).toBeUndefined(); }); it('prevents operations if the "if-match" header does not match.', async(): Promise => { // GET root ETag let response = await getResource(baseUrl); const eTag = response.headers.get('ETag'); expect(typeof eTag).toBe('string'); // POST fails because of header response = await fetch(baseUrl, { method: 'POST', headers: { 'content-type': 'text/plain', 'if-match': '"notAMatchingETag"' }, body: 'TESTFILE', }); expect(response.status).toBe(412); // POST succeeds with correct header response = await fetch(baseUrl, { method: 'POST', headers: { 'content-type': 'text/plain', 'if-match': eTag! }, body: 'TESTFILE1', }); expect(response.status).toBe(201); const documentUrl = response.headers.get('location'); expect(typeof documentUrl).toBe('string'); // DELETE expect(await deleteResource(documentUrl!)).toBeUndefined(); }); it('prevents operations if the "if-none-match" header does match.', async(): Promise => { // GET root ETag let response = await getResource(baseUrl); const eTag = response.headers.get('ETag'); expect(typeof eTag).toBe('string'); // POST fails because of header response = await fetch(baseUrl, { method: 'POST', headers: { 'content-type': 'text/plain', 'if-none-match': eTag! }, body: 'TESTFILE', }); expect(response.status).toBe(412); // POST succeeds with correct header response = await fetch(baseUrl, { method: 'POST', headers: { 'content-type': 'text/plain', 'if-none-match': '"notAMatchingETag"' }, body: 'TESTFILE1', }); expect(response.status).toBe(201); const documentUrl = response.headers.get('location'); expect(typeof documentUrl).toBe('string'); // DELETE expect(await deleteResource(documentUrl!)).toBeUndefined(); }); it('prevents operations if the "if-unmodified-since" header is before the modified date.', async(): Promise => { const documentUrl = `${baseUrl}document3.txt`; // PUT let response = await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE' }); // GET last-modified header response = await getResource(documentUrl); const lastModifiedVal = response.headers.get('last-modified'); expect(typeof lastModifiedVal).toBe('string'); const lastModified = new Date(lastModifiedVal!); const oldDate = new Date(Date.now() - 10000); // DELETE fails because oldDate < lastModified response = await fetch(documentUrl, { method: 'DELETE', headers: { 'if-unmodified-since': oldDate.toUTCString() }, }); expect(response.status).toBe(412); // DELETE succeeds because lastModified date matches response = await fetch(documentUrl, { method: 'DELETE', headers: { 'if-unmodified-since': lastModified.toUTCString() }, }); expect(response.status).toBe(205); }); });