p5.js-web-editor/server/domain-objects/__test__/Project.test.js
Andrew Nicolaou d44a058fd8 Public API: Create new project (fixes #1095) (#1106)
* Converts import script to use public API endpoints

The endpoints don't exist yet, but this is a good way to see how
the implementation of the data structures differ.

* Exposes public API endpoint to fetch user's sketches

* Implements public API delete endpoint

* Adds helper to create custom ApplicationError classes

* Adds create project endpoint that understand API's data structure

This transforms the nested tree of file data into a mongoose
Project model

* Returns '201 Created' to match API spec

* Removes 'CustomError' variable assignment as it shows up in test output

* transformFiles will return file validation errors

* Tests API project controller

* Tests toModel()

* Creates default files if no root-level .html file is provided

* Do not auto-generate a slug if it is provided

Fixes a bug where the slug was auto-generated using the sketch name,
even if a slug property had been provided.

* Validates uniqueness of slugs for projects created by the public API

* Adds tests for slug uniqueness

* Configures node's Promise implementation for mongoose (fixes warnings)

* Moves createProject tests to match controller location

* Adds support for code to ApplicationErrors

* deleteProject controller tests

* getProjectsForUser controller tests

- implements tests
- update apiKey tests to use new User mocks

* Ensure error objects have consistent property names

`message` is used as a high-level description of the errors
`detail` is optional and has an plain language explanation of the
individual errors
`errors` is an array of each individual problem from `detail` in a
machine-readable format

* Assert environment variables are provided at script start

* Version public API

* Expect "files" property to always be provided

* Fixes linting error

* Converts import script to use public API endpoints

The endpoints don't exist yet, but this is a good way to see how
the implementation of the data structures differ.

* Exposes public API endpoint to fetch user's sketches

* Implements public API delete endpoint

* Adds helper to create custom ApplicationError classes

* Adds create project endpoint that understand API's data structure

This transforms the nested tree of file data into a mongoose
Project model

* Returns '201 Created' to match API spec

* Removes 'CustomError' variable assignment as it shows up in test output

* transformFiles will return file validation errors

* Tests API project controller

* Tests toModel()

* Creates default files if no root-level .html file is provided

* Do not auto-generate a slug if it is provided

Fixes a bug where the slug was auto-generated using the sketch name,
even if a slug property had been provided.

* Validates uniqueness of slugs for projects created by the public API

* Adds tests for slug uniqueness

* Configures node's Promise implementation for mongoose (fixes warnings)

* Moves createProject tests to match controller location

* deleteProject controller tests

* Adds support for code to ApplicationErrors

* getProjectsForUser controller tests

- implements tests
- update apiKey tests to use new User mocks

* Ensure error objects have consistent property names

`message` is used as a high-level description of the errors
`detail` is optional and has an plain language explanation of the
individual errors
`errors` is an array of each individual problem from `detail` in a
machine-readable format

* Assert environment variables are provided at script start

* Version public API

* Expect "files" property to always be provided

* Fixes linting error

* Checks that authenticated user has permission to create under this namespace

Previously, the project was always created under the authenticated user's
namespace, but this not obvious behaviour.
2019-08-30 14:26:57 -04:00

385 lines
8.6 KiB
JavaScript

import find from 'lodash/find';
import { containsRootHtmlFile, toModel, transformFiles, FileValidationError } from '../Project';
jest.mock('../../utils/createId');
// TODO: File name validation
// TODO: File extension validation
//
describe('domain-objects/Project', () => {
describe('containsRootHtmlFile', () => {
it('returns true for at least one root .html', () => {
expect(containsRootHtmlFile({ 'index.html': {} })).toBe(true);
expect(containsRootHtmlFile({ 'another-one.html': {} })).toBe(true);
expect(containsRootHtmlFile({ 'one.html': {}, 'two.html': {} })).toBe(true);
expect(containsRootHtmlFile({ 'one.html': {}, 'sketch.js': {} })).toBe(true);
});
it('returns false anything else', () => {
expect(containsRootHtmlFile({ 'sketch.js': {} })).toBe(false);
});
it('ignores nested html', () => {
expect(containsRootHtmlFile({
examples: {
files: {
'index.html': {}
}
}
})).toBe(false);
});
});
describe('toModel', () => {
it('filters extra properties', () => {
const params = {
name: 'My sketch',
extraThing: 'oopsie',
files: {}
};
const model = toModel(params);
expect(model.name).toBe('My sketch');
expect(model.extraThing).toBeUndefined();
});
it('throws FileValidationError', () => {
const params = {
files: {
'index.html': {} // missing content or url
}
};
expect(() => toModel(params)).toThrowError(FileValidationError);
});
it('throws if files is not an object', () => {
const params = {
files: []
};
expect(() => toModel(params)).toThrowError(FileValidationError);
});
it('creates default index.html and dependent files if no root .html is provided', () => {
const params = {
files: {}
};
const { files } = toModel(params);
expect(files.length).toBe(4);
expect(find(files, { name: 'index.html' })).not.toBeUndefined();
expect(find(files, { name: 'sketch.js' })).not.toBeUndefined();
expect(find(files, { name: 'style.css' })).not.toBeUndefined();
});
it('does not create default files if any root .html is provided', () => {
const params = {
files: {
'example.html': {
content: '<html><body>Hello!</body></html>'
}
}
};
const { files } = toModel(params);
expect(files.length).toBe(2);
expect(find(files, { name: 'example.html' })).not.toBeUndefined();
expect(find(files, { name: 'index.html' })).toBeUndefined();
expect(find(files, { name: 'sketch.js' })).toBeUndefined();
expect(find(files, { name: 'style.css' })).toBeUndefined();
});
it('does not overwrite default CSS and JS of the same name if provided', () => {
const params = {
files: {
'sketch.js': {
content: 'const sketch = true;'
},
'style.css': {
content: 'body { outline: 10px solid red; }'
}
}
};
const { files } = toModel(params);
expect(files.length).toBe(4);
expect(find(files, { name: 'index.html' })).not.toBeUndefined();
const sketchFile = find(files, { name: 'sketch.js' });
expect(sketchFile.content).toBe('const sketch = true;');
const cssFile = find(files, { name: 'style.css' });
expect(cssFile.content).toBe('body { outline: 10px solid red; }');
});
});
});
describe('transformFiles', () => {
beforeEach(() => {
// eslint-disable-next-line global-require
const { resetMockCreateId } = require('../../utils/createId');
resetMockCreateId();
});
it('creates an empty root with no data', () => {
const tree = {};
expect(transformFiles(tree)).toEqual([{
_id: '0',
fileType: 'folder',
name: 'root',
children: []
}]);
});
it('converts tree-shaped files into list', () => {
const tree = {
'index.html': {
content: 'some contents',
}
};
expect(transformFiles(tree)).toEqual([
{
_id: '0',
fileType: 'folder',
name: 'root',
children: ['1']
},
{
_id: '1',
content: 'some contents',
fileType: 'file',
name: 'index.html'
}
]);
});
it('uses file url over content', () => {
const tree = {
'script.js': {
url: 'http://example.net/something.js'
}
};
expect(transformFiles(tree)).toEqual([
{
_id: '0',
fileType: 'folder',
name: 'root',
children: ['1']
},
{
_id: '1',
url: 'http://example.net/something.js',
fileType: 'file',
name: 'script.js'
}
]);
});
it('creates folders', () => {
const tree = {
'a-folder': {
files: {}
},
};
expect(transformFiles(tree)).toEqual([
{
_id: '0',
fileType: 'folder',
name: 'root',
children: ['1']
},
{
_id: '1',
children: [],
fileType: 'folder',
name: 'a-folder'
}
]);
});
it('walks the tree processing files', () => {
const tree = {
'index.html': {
content: 'some contents',
},
'a-folder': {
files: {
'data.csv': {
content: 'this,is,data'
},
'another.txt': {
content: 'blah blah'
}
}
},
};
expect(transformFiles(tree)).toEqual([
{
_id: '0',
fileType: 'folder',
name: 'root',
children: ['1', '2']
},
{
_id: '1',
name: 'index.html',
fileType: 'file',
content: 'some contents'
},
{
_id: '2',
name: 'a-folder',
fileType: 'folder',
children: ['3', '4']
},
{
_id: '3',
name: 'data.csv',
fileType: 'file',
content: 'this,is,data'
},
{
_id: '4',
name: 'another.txt',
fileType: 'file',
content: 'blah blah'
}
]);
});
it('handles deep nesting', () => {
const tree = {
first: {
files: {
second: {
files: {
third: {
files: {
'hello.js': {
content: 'world!'
}
}
}
}
}
}
},
};
expect(transformFiles(tree)).toEqual([
{
_id: '0',
fileType: 'folder',
name: 'root',
children: ['1']
},
{
_id: '1',
name: 'first',
fileType: 'folder',
children: ['2']
},
{
_id: '2',
name: 'second',
fileType: 'folder',
children: ['3']
},
{
_id: '3',
name: 'third',
fileType: 'folder',
children: ['4']
},
{
_id: '4',
name: 'hello.js',
fileType: 'file',
content: 'world!'
}
]);
});
it('allows duplicate names in different folder', () => {
const tree = {
'index.html': {
content: 'some contents',
},
'data': {
files: {
'index.html': {
content: 'different file'
}
}
},
};
expect(transformFiles(tree)).toEqual([
{
_id: '0',
fileType: 'folder',
name: 'root',
children: ['1', '2']
},
{
_id: '1',
name: 'index.html',
fileType: 'file',
content: 'some contents'
},
{
_id: '2',
name: 'data',
fileType: 'folder',
children: ['3']
},
{
_id: '3',
name: 'index.html',
fileType: 'file',
content: 'different file'
}
]);
});
it('validates files', () => {
const tree = {
'index.html': {} // missing `content`
};
expect(() => transformFiles(tree)).toThrowError(FileValidationError);
});
it('collects all file validation errors', () => {
const tree = {
'index.html': {}, // missing `content`
'something.js': {} // also missing `content`
};
try {
transformFiles(tree);
// Should not get here
throw new Error('should have thrown before this point');
} catch (err) {
expect(err).toBeInstanceOf(FileValidationError);
expect(err.files).toEqual([
{ name: 'index.html', message: 'missing \'url\' or \'content\'' },
{ name: 'something.js', message: 'missing \'url\' or \'content\'' }
]);
}
});
});