From a7a22bf43a7057c4230e1193dabb255df7a22a2c Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 17 Aug 2021 10:38:02 +0200 Subject: [PATCH] test: Create integration tests for conditions --- test/integration/Conditions.test.ts | 200 ++++++++++++++++++++++++++++ test/util/Util.ts | 1 + 2 files changed, 201 insertions(+) create mode 100644 test/integration/Conditions.test.ts diff --git a/test/integration/Conditions.test.ts b/test/integration/Conditions.test.ts new file mode 100644 index 000000000..eb29b377f --- /dev/null +++ b/test/integration/Conditions.test.ts @@ -0,0 +1,200 @@ +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); + + // 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); + }); +}); diff --git a/test/util/Util.ts b/test/util/Util.ts index 634e12474..9e99e3862 100644 --- a/test/util/Util.ts +++ b/test/util/Util.ts @@ -4,6 +4,7 @@ import type { SystemError } from '../../src/util/errors/SystemError'; const portNames = [ // Integration + 'Conditions', 'DynamicPods', 'Identity', 'LpdHandlerWithAuth',