mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
change: Do not serve index on */*
Closes https://github.com/solid/community-server/issues/844
This commit is contained in:
parent
defdb32a35
commit
c7399654b5
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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<void> => {
|
||||
const response = await getResource(baseUrl, { contentType: 'text/html' });
|
||||
it('returns the root container listing.', async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
// 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<void> => {
|
||||
// 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<void> => {
|
||||
// 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();
|
||||
|
@ -33,11 +33,12 @@ describe('An IndexRepresentationStore', (): void => {
|
||||
.toThrow('Invalid index name');
|
||||
});
|
||||
|
||||
it('retrieves the index resource if it exists.', async(): Promise<void> => {
|
||||
const result = await store.getRepresentation({ path: baseUrl }, {});
|
||||
it('retrieves the index resource if it is explicitly preferred.', async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
@ -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<void> => {
|
||||
it('returns the index resource if the media range is set to */*.', async(): Promise<void> => {
|
||||
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<void> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -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<Response> {
|
||||
export async function getResource(url: string,
|
||||
options?: { accept?: string },
|
||||
expected?: { contentType?: string }): Promise<Response> {
|
||||
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"`);
|
||||
|
Loading…
x
Reference in New Issue
Block a user