Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.acl
/.data
/.eslintcache
/componentsjs-error-state.json
/coverage
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import opinionated from 'opinionated-eslint-config';

export default opinionated().append({
// Don't want to lint test assets
ignores: [ 'test/assets/*', 'componentsjs-error-state.json' ],
ignores: [ 'test/assets/*', 'componentsjs-error-state.json', '.data/**' ],
});
2 changes: 1 addition & 1 deletion src/storage/accessors/FileDataAccessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class FileDataAccessor implements DataAccessor {
try {
return await stat(path);
} catch (error: unknown) {
if (isSystemError(error) && error.code === 'ENOENT') {
if (isSystemError(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
throw new NotFoundHttpError('', { cause: error });
}
throw error;
Expand Down
28 changes: 28 additions & 0 deletions test/integration/FileBackend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,34 @@ describe('A server with a file backend storage', (): void => {
},
);

it(
'returns 201 (not 500) when POSTing with the same Slug twice to a file-backend container.',
async(): Promise<void> => {
const slug = 'duplicate-slug-test.json';
const postOptions = {
method: 'POST',
headers: {
'content-type': 'application/json',
slug,
},
body: '{}',
};

// First POST with the slug β€” should create the resource.
const res1 = await fetch(baseUrl, postOptions);
expect(res1.status).toBe(201);
const location1 = res1.headers.get('location');
expect(location1).toBe(`${baseUrl}${slug}`);

// Second POST with the same slug β€” must NOT return 500.
// The server should fall back to a UUID-based name instead.
const res2 = await fetch(baseUrl, postOptions);
expect(res2.status).toBe(201);
const location2 = res2.headers.get('location');
expect(location2).not.toBe(location1);
},
);

it(
'supports content types for which no extension mapping can be found (and falls back to using .unknown).',
async(): Promise<void> => {
Expand Down
9 changes: 9 additions & 0 deletions test/unit/storage/accessors/FileDataAccessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ describe('A FileDataAccessor', (): void => {
await expect(accessor.getMetadata({ path: base })).rejects.toThrow('error');
});

it('throws a 404 if stat returns ENOTDIR (container path over an existing file).', async(): Promise<void> => {
cache.data = { resource: 'data' };
const enotdir = Object.assign(new Error('ENOTDIR: not a directory, stat'), { code: 'ENOTDIR', syscall: 'stat' });
jest.requireMock('fs-extra').stat = (): never => {
throw enotdir;
};
await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
});

it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => {
cache.data = { resource: 'data' };
await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
Expand Down
Loading