diff --git a/src/storage/IndexRepresentationStore.ts b/src/storage/IndexRepresentationStore.ts index c07eebb90..a6d1606b0 100644 --- a/src/storage/IndexRepresentationStore.ts +++ b/src/storage/IndexRepresentationStore.ts @@ -65,12 +65,18 @@ export class IndexRepresentationStore extends PassthroughStore { } /** - * Makes sure the stored media range matches the highest weight preference. + * Makes sure the stored media range explicitly matches the highest weight preference. */ private matchesPreferences(preferences: RepresentationPreferences): boolean { - const cleaned = cleanPreferences(preferences.type); - const max = Math.max(...Object.values(cleaned)); - return Object.entries(cleaned).some(([ range, weight ]): boolean => - matchesMediaType(range, this.mediaRange) && weight === max); + // Always match */* + if (this.mediaRange === '*/*') { + return true; + } + + // Otherwise, determine if an explicit match has the highest weight + const types = cleanPreferences(preferences.type); + const max = Math.max(...Object.values(types)); + return Object.entries(types).some(([ range, weight ]): boolean => + range !== '*/*' && (max - weight) < 0.01 && matchesMediaType(range, this.mediaRange)); } } diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts index 3ef14817f..d498b0442 100644 --- a/test/integration/LdpHandlerWithoutAuth.test.ts +++ b/test/integration/LdpHandlerWithoutAuth.test.ts @@ -58,12 +58,24 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC await app.stop(); }); - it('can read the root container index page.', async(): Promise => { - const response = await getResource(baseUrl, { contentType: 'text/html' }); + it('returns the root container listing.', async(): Promise => { + const response = await getResource(baseUrl, {}, { contentType: 'text/turtle' }); + + await expect(response.text()).resolves.toContain('ldp:BasicContainer'); + expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`); + }); + + it('returns the root container listing when asking for */*.', async(): Promise => { + const response = await getResource(baseUrl, { accept: '*/*' }, { contentType: 'text/turtle' }); + + await expect(response.text()).resolves.toContain('ldp:BasicContainer'); + expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`); + }); + + it('can read the root container index page when asking for HTML.', async(): Promise => { + const response = await getResource(baseUrl, { accept: 'text/html' }, { contentType: 'text/html' }); await expect(response.text()).resolves.toContain('Welcome to the Community Solid Server'); - - // This is only here because we're accessing the root container expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`); }); @@ -89,7 +101,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE0' }); // GET - const response = await getResource(documentUrl, { contentType: 'text/plain' }); + const response = await getResource(documentUrl, {}, { contentType: 'text/plain' }); await expect(response.text()).resolves.toBe('TESTFILE0'); // DELETE @@ -102,14 +114,14 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE0' }); // GET - let response = await getResource(documentUrl, { contentType: 'text/plain' }); + let response = await getResource(documentUrl, {}, { contentType: 'text/plain' }); await expect(response.text()).resolves.toBe('TESTFILE0'); // PUT await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE1' }); // GET - response = await getResource(documentUrl, { contentType: 'text/plain' }); + response = await getResource(documentUrl, {}, { contentType: 'text/plain' }); await expect(response.text()).resolves.toBe('TESTFILE1'); // DELETE @@ -133,6 +145,32 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC expect(await deleteResource(containerUrl)).toBeUndefined(); }); + it('can create a container and retrieve it.', async(): Promise => { + // Create container + const containerUrl = `${baseUrl}testcontainer0/`; + await putResource(containerUrl, { contentType: 'text/turtle' }); + + // GET representation + const response = await getResource(containerUrl, { accept: '*/*' }, { contentType: 'text/turtle' }); + await expect(response.text()).resolves.toContain('ldp:BasicContainer'); + + // DELETE + expect(await deleteResource(containerUrl)).toBeUndefined(); + }); + + it('can create a container and view it as HTML.', async(): Promise => { + // Create container + const containerUrl = `${baseUrl}testcontainer0/`; + await putResource(containerUrl, { contentType: 'text/turtle' }); + + // GET representation + const response = await getResource(containerUrl, { accept: 'text/html' }, { contentType: 'text/html' }); + await expect(response.text()).resolves.toContain('Contents of testcontainer0'); + + // DELETE + expect(await deleteResource(containerUrl)).toBeUndefined(); + }); + it('can create a container and put a document in it.', async(): Promise => { // Create container const containerUrl = `${baseUrl}testcontainer0/`; @@ -143,7 +181,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE0' }); // GET document - const response = await getResource(documentUrl, { contentType: 'text/plain' }); + const response = await getResource(documentUrl, {}, { contentType: 'text/plain' }); await expect(response.text()).resolves.toBe('TESTFILE0'); // DELETE @@ -227,7 +265,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC await expect(response.text()).resolves.toHaveLength(0); // GET - await getResource(documentUrl, { contentType: 'image/png' }); + await getResource(documentUrl, {}, { contentType: 'image/png' }); // DELETE expect(await deleteResource(documentUrl)).toBeUndefined(); diff --git a/test/unit/storage/IndexRepresentationstore.test.ts b/test/unit/storage/IndexRepresentationstore.test.ts index 01b748225..44f8335ae 100644 --- a/test/unit/storage/IndexRepresentationstore.test.ts +++ b/test/unit/storage/IndexRepresentationstore.test.ts @@ -33,11 +33,12 @@ describe('An IndexRepresentationStore', (): void => { .toThrow('Invalid index name'); }); - it('retrieves the index resource if it exists.', async(): Promise => { - const result = await store.getRepresentation({ path: baseUrl }, {}); + it('retrieves the index resource if it is explicitly preferred.', async(): Promise => { + const preferences = { type: { 'text/turtle': 0.5, 'text/html': 0.8 }}; + const result = await store.getRepresentation({ path: baseUrl }, preferences); await expect(readableToString(result.data)).resolves.toBe('index data'); expect(source.getRepresentation).toHaveBeenCalledTimes(2); - expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}index.html` }, {}, undefined); + expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}index.html` }, preferences, undefined); expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, {}, undefined); // Use correct metadata @@ -45,18 +46,54 @@ describe('An IndexRepresentationStore', (): void => { expect(result.metadata.contentType).toBe('text/html'); }); + it('retrieves the index resource if there is a range preference.', async(): Promise => { + const preferences = { type: { 'text/*': 0.8, 'other/other': 0.7 }}; + const result = await store.getRepresentation({ path: baseUrl }, preferences); + await expect(readableToString(result.data)).resolves.toBe('index data'); + expect(source.getRepresentation).toHaveBeenCalledTimes(2); + expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}index.html` }, preferences, undefined); + expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, {}, undefined); + + // Use correct metadata + expect(result.metadata.identifier.value).toBe(baseUrl); + expect(result.metadata.contentType).toBe('text/html'); + }); + + it('does not retrieve the index resource if there are no type preferences.', async(): Promise => { + const preferences = {}; + const result = await store.getRepresentation({ path: baseUrl }, preferences); + await expect(readableToString(result.data)).resolves.toBe('container data'); + expect(source.getRepresentation).toHaveBeenCalledTimes(1); + expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, preferences, undefined); + + // Use correct metadata + expect(result.metadata.identifier.value).toBe(baseUrl); + expect(result.metadata.contentType).toBe('text/turtle'); + }); + + it('does not retrieve the index resource on */*.', async(): Promise => { + const preferences = { type: { '*/*': 1 }}; + const result = await store.getRepresentation({ path: baseUrl }, preferences); + await expect(readableToString(result.data)).resolves.toBe('container data'); + }); + it('errors if a non-404 error was thrown when accessing the index resource.', async(): Promise => { + const preferences = { type: { 'text/turtle': 0.5, 'text/html': 0.8 }}; source.getRepresentation.mockRejectedValueOnce(new ConflictHttpError('conflict!')); - await expect(store.getRepresentation({ path: baseUrl }, {})).rejects.toThrow('conflict!'); + await expect(store.getRepresentation({ path: baseUrl }, preferences)).rejects.toThrow('conflict!'); expect(source.getRepresentation).toHaveBeenCalledTimes(1); }); it('requests the usual data if there is no index resource.', async(): Promise => { - const result = await store.getRepresentation(emptyContainer, {}); + const preferences = { type: { 'text/turtle': 0.5, 'text/html': 0.8 }}; + source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError()); + const result = await store.getRepresentation(emptyContainer, preferences); await expect(readableToString(result.data)).resolves.toBe('container data'); expect(source.getRepresentation).toHaveBeenCalledTimes(2); - expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${emptyContainer.path}index.html` }, {}, undefined); - expect(source.getRepresentation).toHaveBeenLastCalledWith(emptyContainer, {}, undefined); + expect(source.getRepresentation).toHaveBeenCalledWith( + { path: `${emptyContainer.path}index.html` }, preferences, undefined, + ); + expect(source.getRepresentation).toHaveBeenLastCalledWith(emptyContainer, preferences, undefined); }); it('requests the usual data if the index media range is not the most preferred.', async(): Promise => { @@ -67,7 +104,7 @@ describe('An IndexRepresentationStore', (): void => { expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, preferences, undefined); }); - it('always returns the index resource if the media range is set to */*.', async(): Promise => { + it('returns the index resource if the media range is set to */*.', async(): Promise => { store = new IndexRepresentationStore(source, 'base.html', '*/*'); // Mocking because we also change the index name source.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('index data', 'text/html')); @@ -83,4 +120,17 @@ describe('An IndexRepresentationStore', (): void => { expect(result.metadata.identifier.value).toBe(baseUrl); expect(result.metadata.contentType).toBe('text/html'); }); + + it('returns the index resource if media range and Accept header are */*.', async(): Promise => { + store = new IndexRepresentationStore(source, 'base.html', '*/*'); + // Mocking because we also change the index name + source.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('index data', 'text/html')); + + const preferences = { type: { '*/*': 1 }}; + const result = await store.getRepresentation({ path: baseUrl }, preferences); + await expect(readableToString(result.data)).resolves.toBe('index data'); + expect(source.getRepresentation).toHaveBeenCalledTimes(2); + expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}base.html` }, preferences, undefined); + expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, {}, undefined); + }); }); diff --git a/test/util/FetchUtil.ts b/test/util/FetchUtil.ts index 1c82adc26..5b156a675 100644 --- a/test/util/FetchUtil.ts +++ b/test/util/FetchUtil.ts @@ -9,9 +9,11 @@ import { LDP } from '../../src/util/Vocabularies'; /** * This is specifically for GET requests which are expected to succeed. */ -export async function getResource(url: string, expected?: { contentType?: string }): Promise { +export async function getResource(url: string, + options?: { accept?: string }, + expected?: { contentType?: string }): Promise { const isContainer = isContainerPath(url); - const response = await fetch(url); + const response = await fetch(url, { headers: options }); expect(response.status).toBe(200); expect(response.headers.get('link')).toContain(`<${LDP.Resource}>; rel="type"`); expect(response.headers.get('link')).toContain(`<${url}.acl>; rel="acl"`);